Skip to content

Bug: Only the last setCookie takes effect on the server route #5464

Closed

Description

@Mr-Alexandre

Which project does this relate to?

Start

Describe the bug

When trying to set multiple cookies in a server handler using setCookie, only the last cookie gets actually set in the browser. All previous setCookie calls seem to be overridden or ignored.

Your Example Website or App

https://github.com/Mr-Alexandre/bug-repro-tanstack-start-cookie

Steps to Reproduce the Bug or Issue

  1. Start the dev server (e.g., pnpm dev).
  2. Open your browser and go to http://localhost:3000/api/set-cookies.
  3. Open DevTools → Network tab → Select the request to /api/set-cookies.
  4. Check the "Set-Cookie" headers in the Response Headers.
  5. You will see that only the last cookie is set.

Expected behavior

As a user, I expected multiple Set-Cookie headers to be sent and both cookies to be set, but I am seeing only the last one is applied. The previous one is missing.

Screenshots or Videos

CleanShot.2025-10-13.at.17.46.16.mp4

Platform

  • Router / Start Version: [e.g. 1.121.0]
  • OS: [macOS]
  • Browser: [Chrome]
  • Browser Version: [140.0.7339.214]
  • Bundler: [vite]
  • Bundler Version: [7.1.7]

Additional context

This bug happens consistently and prevents setting multiple cookies for auth/session scenarios. Possibly related to how Response headers are managed in TanStack Start server handlers.

Activity

  1. juliusdelta commented on Oct 13, 2025

    @juliusdelta

    I cannot reproduce this issue from the bug example repo with the following browsers:

    Zen Browser (Firefox based) 1.16.4b (Firefox 143.0.4) (64-bit)
    Image

    Chromium Version 141.0.7390.76 (Official Build) Arch Linux (64-bit)
    Image

    OS: Arch Linux x86_64 (Kernal Linux 6.12.51-1-lts)

    It's possible I'm missing something though, just thought I'd give it a shot to see if I can track down what's happening in the H3 setCookie call but like I said, I was unable to reproduce.

  2. Mr-Alexandre commented on Oct 14, 2025

    @Mr-Alexandre
    Author

    I’ve tested this issue across multiple versions and confirmed that it only appears in the latest releases.

    Works correctly in:
    {
    "@tanstack/react-router": "^1.131.48",
    "@tanstack/react-router-devtools": "^1.131.48",
    "@tanstack/react-router-ssr-query": "^1.131.48",
    "@tanstack/react-start": "^1.131.48",
    "@tanstack/router-plugin": "^1.131.48"
    }

    Bug reproduced in:
    {
    "@tanstack/react-router": "^1.132.0",
    "@tanstack/react-router-devtools": "^1.132.0",
    "@tanstack/react-router-ssr-query": "^1.131.7",
    "@tanstack/react-start": "^1.132.0",
    "@tanstack/router-plugin": "^1.132.0"
    }

    Tested on:
    • Device: MacBook Pro M4 Pro
    • OS: macOS 15.6.1
    • Browsers:
    • Chrome 141.0.7390.77
    • Safari 18.6 (20621.3.11.11.3)

    Chrome 141.0.7390.77
    Image

    Safari 18.6 (20621.3.11.11.3)
    Image

  3. Mr-Alexandre commented on Oct 16, 2025

    @Mr-Alexandre
    Author

    I’ve identified the root cause more precisely. The issue only occurs when using Node.js v20.19.2.

    After upgrading to Node.js v22.2.0, the problem no longer occurs — both cookies are properly set and received by the browser.

    ✅ Working:
    • Node.js v22.2.0 – ✅ multiple Set-Cookie headers work

    ❌ Broken:
    • Node.js v20.19.2 – ❌ only the last Set-Cookie is set

  4. schiller-manuel commented on Oct 16, 2025

    @schiller-manuel
    Collaborator

    we require node v22 at least in start

  5. ecoskun87 commented on Oct 17, 2025

    @ecoskun87

    hi i use BUN 1.3.0 , same bug is here too,

  6. zWingz commented on Nov 27, 2025

    @zWingz

    hi i use BUN 1.3.0 , same bug is here too,

  7. 01dr commented on Mar 16, 2026

    @01dr

    Had something similar on Bun. Updating to 1.166.14 resolved issue for me.

  8. corepay commented on Apr 26, 2026

    @corepay

    Hitting this on a different setup than #7189 (which closed as Bun-on-Linux node:http compat). Reporting in case it adds data.

    Environment

    macOS 15.4.1 / Apple Silicon
    Bun 1.3.10
    @tanstack/react-start 1.167.41
    @tanstack/start-server-core 1.167.19
    srvx 0.11.15, h3-v2 2.0.1-rc.20, vite 8.0.8
    Symptom Server function returns a 200 Response with two Set-Cookie entries on event.res.headers (populated via setResponseHeader("Set-Cookie", [c1, c2]) from @tanstack/start-server-core, OR via two separate setCookie(event, name, value, opts) calls). On the wire only the second cookie survives — same wire pattern as #7189, but on macOS-Bun where #7189 explicitly passes.

    HTTP/1.1 200
    content-type: application/json
    set-cookie: di_refresh=...; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh
    x-tss-raw: true
    (Expected: two set-cookie lines.)

    What works in isolation on this machine

    I added probes to rule out the Bun primitives one layer at a time. All pass:

    Headers[@@iterator] and Headers.entries() yield two separate ["set-cookie", value] entries (not joined).
    Headers.set("x-other", "v") does not disturb the set-cookie list.
    new Headers(otherHeaders) preserves multiple set-cookie entries.
    nodeRes.writeHead(status, statusText, ["set-cookie", "a", "set-cookie", "b", "x-other", "v"]) produces two wire set-cookie lines, even with kOutHeaders pre-populated by an earlier res.appendHeader("Vary", "Sec-Fetch-Dest").
    A bare srvx/node server with the dev-server-plugin shape (new NodeRequest({req, res}) → await fetch(webReq) → sendNodeResponse(res, webRes)) preserves both cookies when the inline handler returns a vanilla Response with two appended Set-Cookie headers.
    So Bun, srvx, and node:http writeHead all behave correctly when invoked directly. The collapse appears only when the request goes through the full TanStack Start request pipeline.

    Static trace through the framework

    For our case (val instanceof Response, val.ok === true, event.res.headers populated):

    start-server-core/server-functions-handler.js:67-71 short-circuits Response returns — sets x-tss-raw: true and returns the user's Response unchanged.
    start-server-core/request-response.js mergeEventResponseHeaders returns early for response.ok === true.
    h3-v2 prepareResponse (line 244-247) calls mergeHeaders$1(val.headers, preparedHeaders, val.headers), which iterates preparedHeaders (Headers object) and target.append("set-cookie", value) per entry.
    srvx/adapters/node.mjs sendNodeResponse for a vanilla Response (no _toNodeResponse) does [...webRes.headers].flat() → writeHead.
    Each step preserves the multi-set-cookie list per the source. Yet the integrated framework loses one.

    Diagnostic probe that matches the wire symptom

    const h = new Headers()
    h.set("content-type", "application/json")
    h.append("set-cookie", "a=1")
    h.append("set-cookie", "b=2")
    console.log(Object.fromEntries(h))
    // → { "content-type": "application/json", "set-cookie": "b=2" }
    Object.fromEntries(headers) collapses set-cookie to the LAST entry — exact match for the wire symptom (only the last-appended cookie survives). I grepped start-server-core, start-plugin-core, react-start-server, start-client-core, srvx, and h3-v2 for Object.fromEntries(...res.headers...) patterns in the runtime hot path and didn't find one, so the actual collapse mechanism is something else with the same effect (likely a Map-keyed-by-name dedup somewhere I haven't pinpointed). But the wire signature is identical.

    Workaround

    Folded both tokens into a single composite cookie carrying a base64url-encoded JSON payload. One Set-Cookie on the wire, no multi-cookie semantics anywhere in the auth path. Works around the issue without depending on any framework fix. Cookie size ~600 bytes for two JWTs — well under the 4 KB browser limit.

    Trade-off accepted: the refresh portion travels on every request rather than being path-scoped to a refresh-only endpoint. HttpOnly + Secure prevent JS access; only the refresh handler decodes the refresh portion server-side.

  9. added a commit that references this issue on Jun 8, 2026
    97e14ca
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions