TanStack / TanStack/router

[start] Unhandled 500 (TypeError: immutable) when a server route returns Response.redirect() while a set-cookie is pending on the event

Open
#7,755 0 comments 1 reaction 0 assignees View on GitHub

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
  1. Add the route above to a Start app.
  2. curl -i http://localhost:3000/api/redirect-repro
  3. Observe 500 with body {"status":500,"unhandled":true,"message":"HTTPError"} instead of a 302.

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.