Skip to content

Commit c796d8b

Browse files
fix: strip Set-Cookie headers from fetch cache entries (#598)
When storing fetch responses in the cache, all response headers were included without filtering. This meant Set-Cookie headers from the original response would be replayed to subsequent requests served from cache, which is incorrect since Set-Cookie is per-response and should not persist across different requests. Both the primary cache write path and the stale-while-revalidate background refresh path now skip Set-Cookie when collecting response headers for the cache entry. Adds a test verifying that the original response retains Set-Cookie but the cached response does not.
1 parent 70e7553 commit c796d8b

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

packages/vinext/src/shims/fetch-cache.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,7 @@ function createPatchedFetch(): typeof globalThis.fetch {
641641
const freshBody = await freshResp.text();
642642
const freshHeaders: Record<string, string> = {};
643643
freshResp.headers.forEach((v, k) => {
644+
if (k.toLowerCase() === "set-cookie") return;
644645
freshHeaders[k] = v;
645646
});
646647

@@ -723,6 +724,9 @@ function createPatchedFetch(): typeof globalThis.fetch {
723724
const body = await cloned.text();
724725
const headers: Record<string, string> = {};
725726
cloned.headers.forEach((v, k) => {
727+
// Never cache Set-Cookie headers — they are per-user and must not
728+
// be replayed to subsequent requests from different users.
729+
if (k.toLowerCase() === "set-cookie") return;
726730
headers[k] = v;
727731
});
728732

tests/fetch-cache.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,4 +2019,35 @@ describe("fetch cache shim", () => {
20192019
expect(fetchMock).toHaveBeenCalledTimes(2);
20202020
});
20212021
});
2022+
2023+
// ── Set-Cookie stripping from cached responses ──────────────────────────
2024+
2025+
describe("Set-Cookie header stripping", () => {
2026+
it("does not include Set-Cookie in cached response headers", async () => {
2027+
fetchMock.mockImplementationOnce(async () => {
2028+
return new Response(JSON.stringify({ ok: true }), {
2029+
status: 200,
2030+
headers: {
2031+
"content-type": "application/json",
2032+
"set-cookie": "session=abc123; Path=/; HttpOnly",
2033+
"x-custom": "keep-me",
2034+
},
2035+
});
2036+
});
2037+
2038+
// First request — response has Set-Cookie
2039+
const res1 = await fetch("https://api.example.com/set-cookie-test", {
2040+
next: { revalidate: 300 },
2041+
});
2042+
expect(res1.headers.get("set-cookie")).toBe("session=abc123; Path=/; HttpOnly");
2043+
expect(res1.headers.get("x-custom")).toBe("keep-me");
2044+
2045+
// Second request — served from cache, Set-Cookie must be absent
2046+
const res2 = await fetch("https://api.example.com/set-cookie-test", {
2047+
next: { revalidate: 300 },
2048+
});
2049+
expect(res2.headers.get("set-cookie")).toBeNull();
2050+
expect(res2.headers.get("x-custom")).toBe("keep-me");
2051+
});
2052+
});
20222053
});

0 commit comments

Comments
 (0)