You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bug: Only the last setCookie takes effect on the server route #5464
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.
Open DevTools → Network tab → Select the request to /api/set-cookies.
Check the "Set-Cookie" headers in the Response Headers.
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.
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)
Chromium Version 141.0.7390.76 (Official Build) Arch Linux (64-bit)
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 H3setCookie call but like I said, I was unable to reproduce.
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.
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.
Activity
juliusdelta commentedon Oct 13, 2025
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)

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

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
H3setCookiecall but like I said, I was unable to reproduce.Mr-Alexandre commentedon Oct 14, 2025
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

Safari 18.6 (20621.3.11.11.3)

Mr-Alexandre commentedon Oct 16, 2025
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
schiller-manuel commentedon Oct 16, 2025
we require node v22 at least in start
ecoskun87 commentedon Oct 17, 2025
hi i use BUN 1.3.0 , same bug is here too,
zWingz commentedon Nov 27, 2025
hi i use BUN 1.3.0 , same bug is here too,
01dr commentedon Mar 16, 2026
Had something similar on Bun. Updating to 1.166.14 resolved issue for me.
corepay commentedon Apr 26, 2026
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.
response reconciliation