Skip to content

Header missing when returning raw response #304

@hdsuperman

Description

@hdsuperman

It has been confirmed that this issues does not occur in the bun environment or the AWS Lambda environment, but only when using hono/node-server.

This issue can be reproduced when using the cors or compress middleware.

import { Hono } from 'hono';
import { compress } from 'hono/compress';

const app = new Hono();

app.use(compress());

app.post('/test', async c => {
  const res = new Response('hello', { status: 200, headers: { 'Content-Type': 'application/json' } });
  res.headers.append('Set-Cookie', 'session=abc; Path=/; HttpOnly');
  return res;
});

export default app;

It seems the raw response header was lost when the middleware modified the header.


Claude Code

When cloning a Response2 instance via new Response(body, existingResponse), headers that were appended after
construction (e.g. Set-Cookie via response.headers.append()) are silently dropped.

Root cause:

In the Response2 constructor, when init is another Response2 instance and no responseCache exists, headers are read
from init.#init.headers (the original ResponseInit passed at construction time) instead of init.headers (the current
headers via the getter, which includes all subsequent mutations):

  // current (buggy)
  if (init instanceof _Response) {
    const cachedGlobalResponse = init[responseCache];
    if (cachedGlobalResponse) {
      this.#init = cachedGlobalResponse;
      this[getResponseCache]();
      return;
    } else {
      this.#init = init.#init;
      headers = new Headers(init.#init.headers); // ← reads stale, original headers
    }
  }

Response2 stores headers in two places:

  • this[cacheKey][2] — the live Headers object returned by the headers getter, reflecting all mutations (.append(),
    .set(), .delete())
  • this.#init.headers — the original ResponseInit.headers passed to the constructor, never updated

The clone reads from #init.headers, so any header added after construction is lost.

Reproduction:

  const res = new Response('hello', { status: 200, headers: { 'Content-Type': 'application/json' } });
  res.headers.append('Set-Cookie', 'session=abc; Path=/; HttpOnly');

  const cloned = new Response(res.body, res);
  console.log(cloned.headers.get('set-cookie')); // null — should be "session=abc; Path=/; HttpOnly"

This does not reproduce with the native Response (e.g. in Lambda / Workers runtimes), only with @hono/node-server's
Response2 override.

Impact:

Any Hono middleware that clones the response after finalization (e.g. cors() calling c.header() which triggers new
Response(this.#res.body, this.#res)) will lose Set-Cookie headers set by route handlers via raw Response objects.
This breaks cookie-based authentication in local Node.js development while working fine in production on
Workers/Lambda.

Fix:

    } else {
      this.#init = init.#init;
  -   headers = new Headers(init.#init.headers);
  +   headers = new Headers(init.headers);
    }

init.headers goes through the getter, which returns the live Headers from cacheKey[2] containing all mutations. new
Headers(...) still creates a new independent copy, so parent and child do not share the same object.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions