From c88f83729a51090ee3b68c4218144d8e337d7fd2 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Fri, 10 Apr 2026 12:01:29 -0500 Subject: [PATCH] fix: strip Set-Cookie from App Route ISR cache entries App Route ISR cache values were persisting Set-Cookie headers from cacheable GET route handlers and replaying them on later HIT/STALE responses. Cached route handler responses are shared across requests, so Set-Cookie must never be stored in the route ISR cache. Fix: - drop Set-Cookie entirely in buildAppRouteCacheValue() - keep X-Vinext-Cache and Cache-Control excluded as before - add a dedicated static route handler fixture with revalidate=1 that sets Set-Cookie directly on the response - add a regression test proving the MISS response includes Set-Cookie but the cached HIT response does not --- .../src/server/app-route-handler-response.ts | 7 +++---- tests/app-router.test.ts | 19 +++++++++++++++++++ .../app/api/static-set-cookie/route.ts | 17 +++++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/app-basic/app/api/static-set-cookie/route.ts diff --git a/packages/vinext/src/server/app-route-handler-response.ts b/packages/vinext/src/server/app-route-handler-response.ts index 2dc7f3c04..68d6f4f39 100644 --- a/packages/vinext/src/server/app-route-handler-response.ts +++ b/packages/vinext/src/server/app-route-handler-response.ts @@ -89,13 +89,12 @@ export async function buildAppRouteCacheValue(response: Response): Promise { + // Never persist Set-Cookie into shared ISR cache entries. Cached route + // handler responses are replayed across requests, so Set-Cookie would leak + // per-request state to later visitors. if (key === "set-cookie" || key === "x-vinext-cache" || key === "cache-control") return; headers[key] = value; }); - const setCookies = response.headers.getSetCookie?.() ?? []; - if (setCookies.length > 0) { - headers["set-cookie"] = setCookies; - } return { kind: "APP_ROUTE", diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index a62b86149..26c2c5a8a 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -2133,6 +2133,25 @@ describe("App Router Production server (startProdServer)", () => { expect(res2.headers.get("x-vinext-cache")).toBeNull(); }); + it("route handler ISR: cached HIT responses do not replay Set-Cookie headers", async () => { + const res1 = await fetch(`${baseUrl}/api/static-set-cookie`); + expect(res1.status).toBe(200); + expect(res1.headers.get("x-vinext-cache")).toBe("MISS"); + const missSetCookies = res1.headers.getSetCookie(); + expect(missSetCookies.length).toBeGreaterThan(0); + expect(missSetCookies[0]).toContain("session="); + + const body1 = await res1.json(); + + const res2 = await fetch(`${baseUrl}/api/static-set-cookie`); + expect(res2.status).toBe(200); + expect(res2.headers.get("x-vinext-cache")).toBe("HIT"); + expect(res2.headers.getSetCookie()).toEqual([]); + + const body2 = await res2.json(); + expect(body2.timestamp).toBe(body1.timestamp); + }); + it("route handler ISR: STALE serves stale data and triggers background regen", async () => { // /api/static-data has revalidate=1 // Cache may already be warm from earlier tests — ensure we have a known timestamp diff --git a/tests/fixtures/app-basic/app/api/static-set-cookie/route.ts b/tests/fixtures/app-basic/app/api/static-set-cookie/route.ts new file mode 100644 index 000000000..b6925445c --- /dev/null +++ b/tests/fixtures/app-basic/app/api/static-set-cookie/route.ts @@ -0,0 +1,17 @@ +export const revalidate = 1; + +export async function GET() { + return new Response( + JSON.stringify({ + timestamp: Date.now(), + message: "static route handler with direct set-cookie header", + }), + { + status: 200, + headers: { + "content-type": "application/json", + "set-cookie": `session=${Date.now()}; Path=/; HttpOnly`, + }, + }, + ); +}