Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/redirect-immutable-headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/start-server-core': patch
---

fix(start-server-core): merge pending set-cookie headers into `Response.redirect()` without throwing

`Response.redirect()` returns a response whose headers are immutable per spec, so merging pending event cookies into it crashed the request with `TypeError: Headers are immutable`. The merge now copies the response when its headers can't be modified, so redirects returned from handlers keep their cookies instead of turning into a 500.

Fixes #7755.
32 changes: 24 additions & 8 deletions packages/start-server-core/src/request-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,40 @@ function getSetCookieValues(headers: Headers): Array<string> {
return value ? [value] : []
}

function mergeEventResponseHeaders(response: Response, event: H3Event): void {
function mergeEventResponseHeaders(
response: Response,
event: H3Event,
): Response {
if (response.ok) {
return
return response
}

const eventSetCookies = getSetCookieValues(event.res.headers)
if (eventSetCookies.length === 0) {
return
return response
}

const responseSetCookies = getSetCookieValues(response.headers)
response.headers.delete('set-cookie')
let target = response
try {
response.headers.delete('set-cookie')
} catch {
// Response.redirect() responses have immutable headers per spec,
// so merge into a mutable copy instead
target = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
})
target.headers.delete('set-cookie')
}
for (const cookie of responseSetCookies) {
response.headers.append('set-cookie', cookie)
target.headers.append('set-cookie', cookie)
}
for (const cookie of eventSetCookies) {
response.headers.append('set-cookie', cookie)
target.headers.append('set-cookie', cookie)
}
return target
}

function attachResponseHeaders<T>(
Expand All @@ -105,14 +121,14 @@ function attachResponseHeaders<T>(
if (isPromiseLike(value)) {
return value.then((resolved) => {
if (resolved instanceof Response) {
mergeEventResponseHeaders(resolved, event)
return mergeEventResponseHeaders(resolved, event) as T
}
return resolved
})
}

if (value instanceof Response) {
mergeEventResponseHeaders(value, event)
return mergeEventResponseHeaders(value, event) as T
}

return value
Expand Down
37 changes: 37 additions & 0 deletions packages/start-server-core/tests/request-response.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { requestHandler, setCookie } from '../src/request-response'

// https://github.com/TanStack/router/issues/7755
describe('requestHandler set-cookie merging', () => {
it('merges pending event cookies into a Response.redirect() without throwing', async () => {
const handler = requestHandler(() => {
setCookie('session', 'abc123')
// Response.redirect() has immutable headers per spec
return Response.redirect('https://example.com/next', 302)
})

const response = await handler(new Request('https://example.com/'), {})

expect(response.status).toBe(302)
expect(response.headers.get('location')).toBe('https://example.com/next')
expect(response.headers.getSetCookie()).toContain('session=abc123; Path=/')
})

it('still merges cookies into a mutable non-ok response', async () => {
const handler = requestHandler(() => {
setCookie('session', 'abc123')
return new Response(null, {
status: 302,
headers: { location: '/next', 'set-cookie': 'from-response=1' },
})
})

const response = await handler(new Request('https://example.com/'), {})

expect(response.status).toBe(302)
expect(response.headers.getSetCookie()).toEqual([
'from-response=1',
'session=abc123; Path=/',
])
})
})