Skip to content

Multiple Set-Cookie headers not forwarded in framework mode, only last cookie sent to client #13657

Closed
@0xcybertim

Description

@0xcybertim

I'm using React Router as a...

framework

Description

When using React Router v7 (framework mode) on Node.js 18+ with native fetch and no polyfills, only the last Set-Cookie header is sent to the client, even though multiple are appended in the action/loader.

--

Steps to Reproduce

  1. In an action, append two cookies:
    const headers = new Headers();
    headers.append('Set-Cookie', 'cookie1=foo; Path=/; SameSite=Lax');
    headers.append('Set-Cookie', 'cookie2=bar; Path=/; SameSite=Lax');
    return new Response(null, { status: 204, headers });
  2. In server.js, use:
    import { createRequestListener } from '@react-router/node';
    app.all('*', createRequestListener({ build, mode }));
  3. Trigger the action from the browser.
  4. Inspect the response headers in the browser’s network tab.

What I Already Tried (and Did NOT Work)

  • Node.js 22+ (undici/native fetch)
  • No fetch polyfills (node-fetch, cross-fetch, etc.) anywhere in the codebase.
  • installGlobals({ nativeFetch: true }) at the top of the server entry (tried both with and without this, no difference).
  • Correct middleware usage:
    app.all('*name', createRequestListener({ build, mode }));
  • Checked the React Router source code and saw it uses getSetCookie() if available.
  • Checked the browser network tab:
    Only one Set-Cookie header is present in the response.
  • Tried making a custom request handler in Express to manually forward all Set-Cookie headers:
    • Attempted to call createRequestListener and await a Response object to extract headers.
    • Attempted to patch res.setHeader and res.end to log and forward all Set-Cookie headers.
    • Attempted to use response.headers.getSetCookie() and fallback to iterating over headers.
    • All custom handler attempts failed because createRequestListener does not return a Response object (it is middleware), and/or only the last Set-Cookie header is ever available.
    • Any attempt to intercept the Response object in userland results in undefined errors or only the last cookie being sent.

Where Exactly It Goes Wrong

  • The React Router server runtime code (headers.ts#L101) is supposed to use getSetCookie() to forward all cookies if available.
  • In practice, even with undici/native fetch and all conditions met, only the last Set-Cookie header is sent to the client.
  • My logs show both cookies are appended in the action, but only one is present in the actual HTTP response.
  • This suggests the bug is in how the framework serializes or forwards the headers from the Response object to the actual HTTP response.
  • Custom request handler attempts in userland cannot fix this, as the Response object is not exposed.

Request

Please investigate why only one Set-Cookie header is sent. If there is a workaround or a recommended pattern for this use case, then I would like to learn about this.

Environment

  • React Router version:
  • @react-router/node version:
  • Node.js version: 22.14.0
  • Express version:
  • OS: macOS (Darwin 24.2.0)
  • No fetch polyfills (node-fetch, cross-fetch, etc.) in use
  • Using undici/native fetch (Node 18+)

System Info

System:
    OS: macOS 15.2
    CPU: (10) arm64 Apple M1 Pro
    Memory: 85.64 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.14.0 - ~/.nvm/versions/node/v22.14.0/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.9.2 - ~/.nvm/versions/node/v22.14.0/bin/npm
  Browsers:
    Chrome: 136.0.7103.114
    Safari: 18.2
  npmPackages:
    @react-router/dev: ^7.2.0 => 7.4.0 
    @react-router/node: ^7.2.0 => 7.4.0 
    @react-router/serve: ^7.2.0 => 7.4.0 
    react-router: ^7.2.0 => 7.4.0 
    vite: ^6.0.0 => 6.2.2

Used Package Manager

npm

Expected Behavior

Both cookies should be present in the response headers:

Set-Cookie: cookie1=foo; Path=/; SameSite=Lax
Set-Cookie: cookie2=bar; Path=/; SameSite=Lax

Actual Behavior

Only the last cookie is present:

Set-Cookie: cookie2=bar; Path=/; SameSite=Lax

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions