[start] Unhandled 500 (TypeError: immutable) when a server route returns Response.redirect() while a set-cookie is pending on the event
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 15.1k
- Forks
- 1.9k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 143
Description
Which project does this relate to?
Start
Describe the bug
When a server route handler returns a Response whose headers are immutable — most commonly Response.redirect(url), but also a passed-through fetch() response — and a cookie is pending on the h3 event (via setCookie(), or indirectly via better-auth's tanstackStartCookies() plugin which calls setCookie after every auth API call), the request fails with an unhandled 500 instead of returning the redirect:
{"status":500,"unhandled":true,"message":"HTTPError"}
Root cause is mergeEventResponseHeaders in @tanstack/start-server-core (src/request-response.ts):
function mergeEventResponseHeaders(response, event) {
if (response.ok) return;
const eventSetCookies = getSetCookieValues(event.res.headers);
if (eventSetCookies.length === 0) return;
const responseSetCookies = getSetCookieValues(response.headers);
response.headers.delete("set-cookie"); // ← throws TypeError: immutable
...
}
A 3xx redirect is not response.ok, so the merge path runs, and Response.redirect() returns a response with an immutable headers guard per the Fetch spec. The TypeError propagates as an unhandled HTTPError 500.
This is easy to hit in production without realizing: any authenticated server route that ends in Response.redirect(...) breaks as soon as the auth library refreshes a session cookie during the request (better-auth with cookieCache does this on every getSession). The handler completes successfully — side effects and DB writes land — and then the framework 500s while post-processing the response, which makes it look like a routing/dispatch failure. Meanwhile unauthenticated requests to the same route (no pending cookie → early return) work fine, which is very confusing to debug.
Your Example Website or App
Minimal repro route below (any Start app):
// src/routes/api/redirect-repro.ts
import { createFileRoute } from "@tanstack/react-router";
import { setCookie } from "@tanstack/react-start/server";
export const Route = createFileRoute("/api/redirect-repro")({
server: {
handlers: {
GET: () => {
setCookie("example", "value"); // anything that puts a pending cookie on the event
return Response.redirect("https://example.com/", 302);
},
},
},
});
Steps to Reproduce the Bug or Issue
- Add the route above to a Start app.
curl -i http://localhost:3000/api/redirect-repro- Observe
500with body{"status":500,"unhandled":true,"message":"HTTPError"}instead of a302.
Also reproducible directly against the dist internals:
import { requestHandler, setCookie } from "@tanstack/start-server-core/dist/esm/request-response.js";
const handler = requestHandler(async () => {
setCookie("example", "value");
return Response.redirect("https://example.com/", 302);
});
const res = await handler(new Request("http://localhost/api/x"));
console.log(res.status); // 500, cause: TypeError: immutable at mergeEventResponseHeaders
Expected behavior
The redirect should be returned (with the pending set-cookie headers merged). When the returned response has immutable headers, mergeEventResponseHeaders could clone it instead of mutating in place, e.g.:
// new Response(response.body, response) yields mutable headers
or guard the mutation with a try/catch fallback to a cloned response.
Platform
- OS: macOS / Cloudflare Workers (workerd) — reproduces on both Node (undici) and workerd, since both enforce the immutable headers guard
@tanstack/react-start: 1.168.27@tanstack/start-server-core: 1.169.16
Additional context
Workaround for anyone hitting this: return new Response(null, { status: 302, headers: { location: url } }) instead of Response.redirect(url) from server route handlers.
Possibly related to the change that introduced event-cookie merging for non-2xx responses (#7189 / #5464) — the merge itself is desirable, it just can't assume the handler's response headers are mutable.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in @tanstack/start-server-core/src/request-response.ts at mergeEventResponseHeaders, then reproduce the issue with the provided redirect-repro route or requestHandler example. Done means a response with pending cookies can return a 302 redirect without an unhandled 500, including when its headers are immutable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100