diff --git a/.changeset/fix-set-cookie-304.md b/.changeset/fix-set-cookie-304.md new file mode 100644 index 000000000000..358ae26dc01f --- /dev/null +++ b/.changeset/fix-set-cookie-304.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve multiple `Set-Cookie` headers on 304 responses diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 6a7395b40b44..38e31e38376a 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -511,19 +511,18 @@ export async function internal_respond(request, options, manifest, state) { if (if_none_match_value === etag) { const headers = new Headers({ etag }); - // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + set-cookie - for (const key of [ - 'cache-control', - 'content-location', - 'date', - 'expires', - 'vary', - 'set-cookie' - ]) { + // https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + for (const key of ['cache-control', 'content-location', 'date', 'expires', 'vary']) { const value = response.headers.get(key); if (value) headers.set(key, value); } + // `Headers.get('set-cookie')` collapses multiple values into a single + // comma-joined string that browsers cannot parse correctly + for (const cookie of response.headers.getSetCookie()) { + headers.append('set-cookie', cookie); + } + return new Response(undefined, { status: 304, headers diff --git a/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag-multiple/+page.server.js b/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag-multiple/+page.server.js new file mode 100644 index 000000000000..eaa867774f12 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag-multiple/+page.server.js @@ -0,0 +1,6 @@ +/** @type {import('./$types').PageServerLoad} */ +export function load({ cookies }) { + cookies.set('one', '1', { path: '', httpOnly: false }); + cookies.set('two', '2', { path: '', httpOnly: false }); + cookies.set('three', '3', { path: '', httpOnly: false }); +} diff --git a/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag-multiple/+page.svelte b/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag-multiple/+page.svelte new file mode 100644 index 000000000000..63892ef9685b --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/cookies/forwarded-in-etag-multiple/+page.svelte @@ -0,0 +1,27 @@ + + + + +
{cookies}
diff --git a/packages/kit/test/apps/basics/test/cross-platform/client.test.js b/packages/kit/test/apps/basics/test/cross-platform/client.test.js index 3a9322c1560f..3d888b74113e 100644 --- a/packages/kit/test/apps/basics/test/cross-platform/client.test.js +++ b/packages/kit/test/apps/basics/test/cross-platform/client.test.js @@ -1188,6 +1188,13 @@ test.describe('cookies', () => { await expect(page.locator('p')).toHaveText('foo=bar'); }); + test('etag forwards multiple cookies', async ({ page }) => { + await page.goto('/cookies/forwarded-in-etag-multiple'); + await expect(page.locator('p')).toHaveText('one=1; three=3; two=2'); + await page.locator('button').click(); + await expect(page.locator('p')).toHaveText('one=1; three=3; two=2'); + }); + test("fetch during SSR doesn't un- and re-escape cookies", async ({ page }) => { await page.goto('/cookies/collect-without-re-escaping'); await expect(page.locator('p')).toHaveText('cookie-special-characters="foo"');