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`, + }, + }, + ); +}