Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/start-server-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"@tanstack/router-core": "workspace:*",
"@tanstack/start-client-core": "workspace:*",
"@tanstack/start-storage-context": "workspace:*",
"h3-v2": "npm:h3@2.0.1-rc.2",
"h3-v2": "npm:h3@2.0.1-rc.6",
"seroval": "^1.4.1",
"tiny-invariant": "^1.3.3"
},
Expand Down
61 changes: 60 additions & 1 deletion packages/start-server-core/src/request-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,65 @@ const eventStorage = globalObj[GLOBAL_EVENT_STORAGE_KEY]

export type { ResponseHeaderName, RequestHeaderName }

type HeadersWithGetSetCookie = Headers & {
getSetCookie?: () => Array<string>
}

type MaybePromise<T> = T | Promise<T>

function isPromiseLike<T>(value: MaybePromise<T>): value is Promise<T> {
return typeof (value as Promise<T>).then === 'function'
}

function getSetCookieValues(headers: Headers): Array<string> {
const headersWithSetCookie = headers as HeadersWithGetSetCookie
if (typeof headersWithSetCookie.getSetCookie === 'function') {
return headersWithSetCookie.getSetCookie()
}
const value = headers.get('set-cookie')
return value ? [value] : []
}

function mergeEventResponseHeaders(response: Response, event: H3Event): void {
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')
for (const cookie of responseSetCookies) {
response.headers.append('set-cookie', cookie)
}
for (const cookie of eventSetCookies) {
response.headers.append('set-cookie', cookie)
}
Comment on lines +92 to +98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "request-response.ts" -type f | head -5

Repository: TanStack/router

Length of output: 111


🏁 Script executed:

cat -n packages/start-server-core/src/request-response.ts | sed -n '80,110p'

Repository: TanStack/router

Length of output: 1105


🏁 Script executed:

cat -n packages/start-server-core/src/request-response.ts | head -50

Repository: TanStack/router

Length of output: 1870


🏁 Script executed:

cat -n packages/start-server-core/src/request-response.ts | sed -n '50,90p'

Repository: TanStack/router

Length of output: 1509


Response headers mutation will fail at runtime for immutable Response objects.

The code directly mutates response.headers using delete() and append(). However, Fetch API Response headers are immutable when created via Response.error(), Response.redirect(), or received from fetch(), throwing a TypeError if mutation is attempted. Since the mergeEventResponseHeaders function receives a generic Response parameter with unknown origin, this approach will fail for responses with immutable headers.

Instead of mutating headers directly, create a new Response with merged headers:

const newHeaders = new Headers(response.headers)
newHeaders.delete('set-cookie')
for (const cookie of responseSetCookies) {
  newHeaders.append('set-cookie', cookie)
}
for (const cookie of eventSetCookies) {
  newHeaders.append('set-cookie', cookie)
}
return new Response(response.body, {
  status: response.status,
  statusText: response.statusText,
  headers: newHeaders
})

Alternatively, check if headers are mutable before attempting mutation, with fallback to response reconstruction for immutable headers.

🤖 Prompt for AI Agents
In packages/start-server-core/src/request-response.ts around lines 92 to 98, the
code mutates response.headers (delete/append) which will throw for immutable
Response headers; instead create a new Headers from response.headers, remove the
existing set-cookie entries on that new Headers, append responseSetCookies and
eventSetCookies to it, and return a new Response preserving response.body,
status, and statusText (and any other needed response options) with the merged
Headers; alternatively detect header mutability and fall back to this
reconstruction when headers are immutable.

}

function attachResponseHeaders<T>(
value: MaybePromise<T>,
event: H3Event,
): MaybePromise<T> {
if (isPromiseLike(value)) {
return value.then((resolved) => {
if (resolved instanceof Response) {
mergeEventResponseHeaders(resolved, event)
}
return resolved
})
}

if (value instanceof Response) {
mergeEventResponseHeaders(value, event)
}

return value
}

export function requestHandler<TRegister = unknown>(
handler: RequestHandler<TRegister>,
) {
Expand All @@ -68,7 +127,7 @@ export function requestHandler<TRegister = unknown>(
const response = eventStorage.run({ h3Event }, () =>
handler(request, requestOpts),
)
return h3_toResponse(response, h3Event)
return h3_toResponse(attachResponseHeaders(response, h3Event), h3Event)
}
}

Expand Down
43 changes: 25 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading