Skip to content

[start] Duplicate Set-Cookie headers dropped on Linux (Bun) for 2xx Response returns #7189

Description

@awinogradov

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

  1. Scaffold a TanStack Start app (React).
  2. Add the route above.
  3. Run bun run dev on macOS. Curl the route — the response carries two Set-Cookie headers.
  4. 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 prepareResponsemergeHeaders$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.tsmergeEventResponseHeaders 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions