-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
Describe the bug
SvelteKit's 304 Not Modified response handling collapses multiple Set-Cookie headers into a single comma-joined header, violating RFC 6265 and causing cookie loss. When a route sets multiple cookies and a subsequent request triggers the 304 path (via If-None-Match), browsers receive malformed Set-Cookie headers that result in partial or complete cookie loss, breaking authentication, CSRF protection, and user state management.
The issue is in packages/kit/src/runtime/server/respond.js at lines 485-495. The header copying loop uses Headers.get('set-cookie') which collapses multiple Set-Cookie headers into a comma-joined string, then writes that as a single header via headers.set().
On non-Node adapters (adapter-cloudflare, adapter-vercel, adapter-netlify), this causes complete loss of all cookies after the first. On adapter-node, splitCookiesString() provides partial heuristic-based recovery, so the bug is masked when testing locally with adapter-node.
Reproduction
The bug is in the 304 header-copying code at respond.js:485-495. Save this as repro.mjs and run with node repro.mjs — it reproduces the exact logic from that code path:
// Simulates what respond.js does when building a 304 response
const originalHeaders = new Headers();
originalHeaders.append('set-cookie', 'session=abc123; Path=/; HttpOnly');
originalHeaders.append('set-cookie', 'csrf=xyz789; Path=/; SameSite=Strict');
originalHeaders.append('set-cookie', 'locale=en; Path=/');
originalHeaders.set('etag', '"test123"');
console.log('Original Set-Cookie count:', originalHeaders.getSetCookie().length);
// → 3
// This is what respond.js lines 485-495 do:
const etag = originalHeaders.get('etag');
const headers304 = new Headers({ etag });
for (const key of ['cache-control', 'content-location', 'date', 'expires', 'vary', 'set-cookie']) {
const value = originalHeaders.get(key); // ← .get() collapses Set-Cookie per spec
if (value) headers304.set(key, value);
}
console.log('304 Set-Cookie count:', headers304.getSetCookie().length);
// → 1 (should be 3)
console.log('304 Set-Cookie value:', headers304.getSetCookie());
// → ['session=abc123; Path=/; HttpOnly, csrf=xyz789; Path=/; SameSite=Strict, locale=en; Path=/']Output:
Original Set-Cookie count: 3
304 Set-Cookie count: 1
304 Set-Cookie value: [
'session=abc123; Path=/; HttpOnly, csrf=xyz789; Path=/; SameSite=Strict, locale=en; Path=/'
]
Three cookies become one malformed comma-joined string. Headers.get('set-cookie') joins multiple values per the Fetch spec — getSetCookie() exists specifically to avoid this, but the 304 path doesn't use it.
Note: If you test this with curl against adapter-node, the cookies may appear correct on the wire because Node's HTTP layer applies splitCookiesString() heuristic recovery after SvelteKit hands off the collapsed header. The bug is still present in SvelteKit's output — it's just masked by a downstream workaround that doesn't exist on other adapters.
Logs
No errors — the malformed header is silently served. Browsers parse only the first cookie from the comma-joined value, silently dropping the rest.
System Info
System:
OS: macOS 26.2
CPU: (14) arm64 Apple M4 Pro
Binaries:
Node: 24.13.0
pnpm: 10.28.1
npmPackages:
@sveltejs/kit: ^2.50.2 => 2.53.4
svelte: ^5.51.0 => 5.53.7
vite: ^7.3.1 => 7.3.1
Most severe on: adapter-cloudflare, adapter-vercel, adapter-netlify (no recovery)
Partially mitigated on: adapter-node (splitCookiesString recovery)
Severity
serious, but I can work around it
Additional Information
Fix — replace the header copying with Set-Cookie-aware handling:
// Instead of:
const value = response.headers.get(key);
if (value) headers.set(key, value);
// Use getSetCookie() for set-cookie:
const cookies = response.headers.getSetCookie();
for (const cookie of cookies) {
headers.append('set-cookie', cookie);
}The intermittent nature (only on 304 responses) makes this particularly hard to debug — it appears as "cookies sometimes don't get set" and affects returning users more than new users.