Skip to content

Regression: lightweight Response materialization changes status from 302 to 200 #362

Description

@ljb7977

Summary

This looks like a regression from #357. That PR added a materialization path that spreads this.#init:

new GlobalResponse(this.#body, {
  ...this.#init,
  headers: liveHeaders,
})

This is safe when this.#init is a plain ResponseInit, but unsafe when it is a native Response. Native Response fields such as status and statusText are accessor properties, not own enumerable properties, so object spread drops them:

const native = new Response(null, { status: 302 })
console.log({ ...native }) // {}

As a result, materialization can effectively call new GlobalResponse(body, { headers }), which defaults to status 200.

With overrideGlobalObjects: true (the default), this can make @hono/node-server's lightweight Response lose its status during materialization. This is not specific to redirects: any non-default status from the native Response can be ignored and replaced with 200.

A response created from a native Response:

const lightweight = new Response(nativeResponse.body, nativeResponse)

initially reports 302, but after .body is read it can become 200.

Regression Range

Tested with Node.js 24.16.0 and hono@4.10.6:

@hono/node-server Result
2.0.2 OK: 302 stays 302
2.0.3 Broken: 302 becomes 200
2.0.4 Broken: 302 becomes 200

Reproduction

const NativeResponse = globalThis.Response
const { serve } = require('@hono/node-server')

const server = serve(
  {
    fetch: () => new Response('ok'),
    hostname: '127.0.0.1',
    port: 0,
  },
  () => {}
)
server.close()

const nativeRedirect = new NativeResponse(null, {
  status: 302,
  statusText: '',
  headers: {
    Location: 'https://example.com/',
    'Content-Length': '0',
  },
})

const lightweightRedirect = new Response(nativeRedirect.body, nativeRedirect)

console.log('initial:', lightweightRedirect.status)

const rebuilt = new Response(lightweightRedirect.body, {
  status: lightweightRedirect.status,
  statusText: lightweightRedirect.statusText,
  headers: lightweightRedirect.headers,
})

console.log('after body read:', lightweightRedirect.status)
console.log('rebuilt:', rebuilt.status)

Actual on 2.0.3 / 2.0.4:

initial: 302
after body read: 200
rebuilt: 200

Expected:

initial: 302
after body read: 302
rebuilt: 302

Suggested Fix

Do not spread a native Response as if it were a plain ResponseInit. Read metadata through accessors:

const init =
  this.#init instanceof GlobalResponse
    ? {
        status: this.#init.status,
        statusText: this.#init.statusText,
        headers: liveHeaders ?? this.#init.headers,
      }
    : liveHeaders
      ? { ...this.#init, headers: liveHeaders }
      : this.#init

return new GlobalResponse(this.#body, init)

Related:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions