Describe the bug
When a TanStack Start API route returns a Response that contains two Set-Cookie headers, only one of them reaches the client on Linux + Bun. The same code works correctly on macOS + Bun (same Bun version, same package versions).
Observed while wiring BetterAuth into a TanStack Start app: BetterAuth sets better-auth.session_token and better-auth.session_data on a successful sign-in. On GitHub Actions (ubuntu-latest, Bun 1.3.9) only session_data survives to the browser. On macOS (Bun 1.3.9) both survive.
Your Example Website or App
Minimal reproduction route (drops an extra route into any TanStack Start app scaffold):
// src/routes/api/cookies.ts
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/cookies")({
server: {
handlers: {
POST: async () => {
const headers = new Headers();
headers.append("set-cookie", "a=1; Path=/; HttpOnly; SameSite=Lax");
headers.append("set-cookie", "b=2; Path=/; HttpOnly; SameSite=Lax");
return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
},
},
},
});
Hit it from both environments:
curl -i -X POST http://localhost:3001/api/cookies
I can stand up a full minimal-repro repo if it would help — just wanted to file the observation first.
Steps to Reproduce the Bug or Issue
- Scaffold a TanStack Start app (React).
- Add the route above.
- Run
bun run dev on macOS. Curl the route — the response carries two Set-Cookie headers.
- Run the same code on a GitHub Actions
ubuntu-latest runner (or any Linux + Bun 1.3.9). Curl the route — the response carries one Set-Cookie header (b=2). Cookie a is silently dropped.
Expected behavior
Both Set-Cookie values should reach the client, regardless of OS/runtime. This is critical for any auth library that sets both an opaque session token and a signed session-data cache in the same response (BetterAuth is one such case; see better-auth/better-auth).
Additional context
- Affected versions (from
bun.lock): @tanstack/react-start@1.167.34, @tanstack/react-start-server@1.166.37, @tanstack/start-server-core@1.167.18, h3@2.0.1-rc.20, bun@1.3.9, vite@8.0.8.
- I verified via a debug response header (
x-auth-debug-cookies) set from inside the route handler that response.headers.getSetCookie().length === 2 at the moment the handler returns — so the drop happens after the route handler returns, somewhere between TanStack Start's prepareResponse / h3_toResponse and the wire.
- Explicitly rebuilding the
Response with headers.getSetCookie() + headers.append("set-cookie", …) in the route handler does not fix it on Linux. Both values are in the returned Headers object; one still gets dropped.
- Removing all h3 event-header writers (i.e. not using anything that calls
setCookie from @tanstack/react-start/server) also does not fix it on Linux, so the prepareResponse → mergeHeaders$1 path that merges preparedHeaders into val.headers is not the culprit on its own.
- The suspect lines I looked at:
@tanstack/start-server-core/src/request-response.ts — mergeEventResponseHeaders early-returns on response.ok, and mergeHeaders$1 in h3@2.0.1-rc.20/dist/h3-*.mjs uses target.set(name, value) for non–set-cookie headers which would preserve duplicates, but I did not prove this is where the drop happens.
- Workaround used in our codebase: disable BetterAuth's
cookieCache so only a single session_token cookie is emitted. Confirms the drop is specific to duplicate Set-Cookie entries, not specific to any cookie name or value.
Happy to iterate on a minimal-repro repo or capture a trace if that speeds up triage.
Describe the bug
When a TanStack Start API route returns a
Responsethat contains twoSet-Cookieheaders, only one of them reaches the client on Linux + Bun. The same code works correctly on macOS + Bun (same Bun version, same package versions).Observed while wiring BetterAuth into a TanStack Start app: BetterAuth sets
better-auth.session_tokenandbetter-auth.session_dataon a successful sign-in. On GitHub Actions (ubuntu-latest, Bun 1.3.9) onlysession_datasurvives to the browser. On macOS (Bun 1.3.9) both survive.Your Example Website or App
Minimal reproduction route (drops an extra route into any TanStack Start app scaffold):
Hit it from both environments:
I can stand up a full minimal-repro repo if it would help — just wanted to file the observation first.
Steps to Reproduce the Bug or Issue
bun run devon macOS. Curl the route — the response carries twoSet-Cookieheaders.ubuntu-latestrunner (or any Linux + Bun 1.3.9). Curl the route — the response carries oneSet-Cookieheader (b=2). Cookieais silently dropped.Expected behavior
Both
Set-Cookievalues should reach the client, regardless of OS/runtime. This is critical for any auth library that sets both an opaque session token and a signed session-data cache in the same response (BetterAuth is one such case; see better-auth/better-auth).Additional context
bun.lock):@tanstack/react-start@1.167.34,@tanstack/react-start-server@1.166.37,@tanstack/start-server-core@1.167.18,h3@2.0.1-rc.20,bun@1.3.9,vite@8.0.8.x-auth-debug-cookies) set from inside the route handler thatresponse.headers.getSetCookie().length === 2at the moment the handler returns — so the drop happens after the route handler returns, somewhere between TanStack Start'sprepareResponse/h3_toResponseand the wire.Responsewithheaders.getSetCookie()+headers.append("set-cookie", …)in the route handler does not fix it on Linux. Both values are in the returnedHeadersobject; one still gets dropped.setCookiefrom@tanstack/react-start/server) also does not fix it on Linux, so theprepareResponse→mergeHeaders$1path that mergespreparedHeadersintoval.headersis not the culprit on its own.@tanstack/start-server-core/src/request-response.ts—mergeEventResponseHeadersearly-returns onresponse.ok, andmergeHeaders$1inh3@2.0.1-rc.20/dist/h3-*.mjsusestarget.set(name, value)for non–set-cookieheaders which would preserve duplicates, but I did not prove this is where the drop happens.cookieCacheso only a singlesession_tokencookie is emitted. Confirms the drop is specific to duplicateSet-Cookieentries, not specific to any cookie name or value.Happy to iterate on a minimal-repro repo or capture a trace if that speeds up triage.