From de0e759f1a4f9d4fbb883d70b83a8b7b354604c8 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Thu, 13 Nov 2025 21:21:42 +0100 Subject: [PATCH 1/8] Apply fixHeaders only once --- packages/open-next/src/http/openNextResponse.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/open-next/src/http/openNextResponse.ts b/packages/open-next/src/http/openNextResponse.ts index 3a8187950..ec6e8047b 100644 --- a/packages/open-next/src/http/openNextResponse.ts +++ b/packages/open-next/src/http/openNextResponse.ts @@ -22,6 +22,7 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse { headers: OutgoingHttpHeaders = {}; headersSent = false; _chunks: Buffer[] = []; + headersAlreadyFixed = false; private _cookies: string[] = []; private responseStream?: Writable; @@ -69,7 +70,7 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse { } constructor( - private fixHeaders: (headers: OutgoingHttpHeaders) => void, + private fixHeadersFn: (headers: OutgoingHttpHeaders) => void, private onEnd: (headers: OutgoingHttpHeaders) => Promise, private streamCreator?: StreamCreator, private initialHeaders?: OutgoingHttpHeaders, @@ -271,6 +272,14 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse { * OpenNext specific method */ + fixHeaders(headers: OutgoingHttpHeaders) { + if (this.headersAlreadyFixed) { + return; + } + this.fixHeadersFn(headers); + this.headersAlreadyFixed = true; + } + getFixedHeaders(): OutgoingHttpHeaders { // Do we want to apply this on writeHead? this.fixHeaders(this.headers); From eadd35070e431df4dece0deb749f8f2e7d4fd4e6 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Thu, 13 Nov 2025 21:22:52 +0100 Subject: [PATCH 2/8] Don't fix cache-control headers if they've been changed --- packages/open-next/src/core/routing/util.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index e1ce7d3a7..83fec1941 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -356,6 +356,11 @@ export async function revalidateIfRequired( * @__PURE__ */ export function fixISRHeaders(headers: OutgoingHttpHeaders) { + const regex = /s-maxage=(\d+)/; + // We only apply the fix if the cache-control header contains s-maxage + if(!headers[CommonHeaders.CACHE_CONTROL]?.match(regex)){ + return; + } if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") { headers[CommonHeaders.CACHE_CONTROL] = "private, no-cache, no-store, max-age=0, must-revalidate"; @@ -366,7 +371,6 @@ export function fixISRHeaders(headers: OutgoingHttpHeaders) { // calculate age const age = Math.round((Date.now() - _lastModified) / 1000); // extract s-maxage from cache-control - const regex = /s-maxage=(\d+)/; const cacheControl = headers[CommonHeaders.CACHE_CONTROL]; debug("cache-control", cacheControl, _lastModified, Date.now()); if (typeof cacheControl !== "string") return; From 288cb12fcd6c3173075405bcd246d90e4e2a8ce4 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Thu, 13 Nov 2025 21:35:18 +0100 Subject: [PATCH 3/8] update test --- packages/tests-e2e/tests/appRouter/isr.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/tests-e2e/tests/appRouter/isr.test.ts b/packages/tests-e2e/tests/appRouter/isr.test.ts index 3a9eabda9..eb61c9314 100644 --- a/packages/tests-e2e/tests/appRouter/isr.test.ts +++ b/packages/tests-e2e/tests/appRouter/isr.test.ts @@ -45,13 +45,28 @@ test("headers", async ({ page }) => { return response.status() === 200; }); await page.goto("/isr"); + let hasBeenStale = false; + let hasBeenHit = false; while (true) { const response = await responsePromise; const headers = response.headers(); + expect(headers["cache-control"]).toBe( + "max-age=10, stale-while-revalidate=999", + ); + const cacheHeader = + headers["x-nextjs-cache"] ?? headers["x-opennext-cache"]; + if (cacheHeader === "STALE") { + hasBeenStale = true; + } + if (cacheHeader === "HIT") { + hasBeenHit = true; + } // this was set in middleware - if (headers["cache-control"] === "max-age=10, stale-while-revalidate=999") { + if ( + hasBeenStale && hasBeenHit + ) { break; } await page.waitForTimeout(1000); From 7d0ab8d7803c97ae1e97383082cd2c844094b9ec Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Thu, 13 Nov 2025 21:35:41 +0100 Subject: [PATCH 4/8] linting --- packages/open-next/src/core/routing/util.ts | 2 +- packages/tests-e2e/tests/appRouter/isr.test.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index 83fec1941..1c198ab62 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -358,7 +358,7 @@ export async function revalidateIfRequired( export function fixISRHeaders(headers: OutgoingHttpHeaders) { const regex = /s-maxage=(\d+)/; // We only apply the fix if the cache-control header contains s-maxage - if(!headers[CommonHeaders.CACHE_CONTROL]?.match(regex)){ + if (!headers[CommonHeaders.CACHE_CONTROL]?.match(regex)) { return; } if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") { diff --git a/packages/tests-e2e/tests/appRouter/isr.test.ts b/packages/tests-e2e/tests/appRouter/isr.test.ts index eb61c9314..2479f05eb 100644 --- a/packages/tests-e2e/tests/appRouter/isr.test.ts +++ b/packages/tests-e2e/tests/appRouter/isr.test.ts @@ -64,9 +64,7 @@ test("headers", async ({ page }) => { } // this was set in middleware - if ( - hasBeenStale && hasBeenHit - ) { + if (hasBeenStale && hasBeenHit) { break; } await page.waitForTimeout(1000); From f8733702b3477934a9b45fab6720a94aae1905ee Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Thu, 13 Nov 2025 22:40:13 +0100 Subject: [PATCH 5/8] changeset --- .changeset/large-pants-press.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/large-pants-press.md diff --git a/.changeset/large-pants-press.md b/.changeset/large-pants-press.md new file mode 100644 index 000000000..cafd0b817 --- /dev/null +++ b/.changeset/large-pants-press.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +Fix cache-control header set in middleware being overriden for ISR route From 9c86966eb3e9277717ef019bd32d2b890df49535 Mon Sep 17 00:00:00 2001 From: Nicolas Dorseuil Date: Thu, 13 Nov 2025 23:47:54 +0100 Subject: [PATCH 6/8] fix unit test --- .../tests/core/routing/util.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/tests-unit/tests/core/routing/util.test.ts b/packages/tests-unit/tests/core/routing/util.test.ts index 0b3a6169b..e4ba8dbab 100644 --- a/packages/tests-unit/tests/core/routing/util.test.ts +++ b/packages/tests-unit/tests/core/routing/util.test.ts @@ -748,6 +748,7 @@ describe("fixISRHeaders", () => { it("should set cache-control directive to must-revalidate when x-nextjs-cache is REVALIDATED", () => { const headers: Record = { "x-nextjs-cache": "REVALIDATED", + "cache-control": "s-maxage=10, stale-while-revalidate=86400", }; fixISRHeaders(headers); @@ -771,6 +772,7 @@ describe("fixISRHeaders", () => { it("should set cache-control directive to stale-while-revalidate when x-nextjs-cache is STALE", () => { const headers: Record = { "x-nextjs-cache": "STALE", + "cache-control": "s-maxage=86400", // 1 day }; fixISRHeaders(headers); @@ -778,6 +780,25 @@ describe("fixISRHeaders", () => { "s-maxage=2, stale-while-revalidate=2592000", ); }); + + it("should not modify cache-control when cache-control is missing", () => { + const headers: Record = { + "x-nextjs-cache": "HIT", + }; + fixISRHeaders(headers); + + expect(headers["cache-control"]).toBeUndefined(); + }); + + it("should not modify cache-control when cache-control has no s-maxage", () => { + const headers: Record = { + "x-nextjs-cache": "HIT", + "cache-control": "private, max-age=0", + }; + fixISRHeaders(headers); + + expect(headers["cache-control"]).toBe("private, max-age=0"); + }); }); describe("invalidateCDNOnRequest", () => { From c70390ec495133e6442f1dc84bca5467cf854835 Mon Sep 17 00:00:00 2001 From: conico974 Date: Mon, 17 Nov 2025 19:34:54 +0100 Subject: [PATCH 7/8] Update packages/open-next/src/core/routing/util.ts Co-authored-by: Victor Berchet --- packages/open-next/src/core/routing/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index 1c198ab62..da0134929 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -356,7 +356,7 @@ export async function revalidateIfRequired( * @__PURE__ */ export function fixISRHeaders(headers: OutgoingHttpHeaders) { - const regex = /s-maxage=(\d+)/; + const regex = /s-maxage=\d+/; // We only apply the fix if the cache-control header contains s-maxage if (!headers[CommonHeaders.CACHE_CONTROL]?.match(regex)) { return; From 38664c0066e16fb071c639a628b80193022d85a1 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Tue, 18 Nov 2025 10:04:37 +0100 Subject: [PATCH 8/8] fixup! refactor --- packages/open-next/src/core/routing/util.ts | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index da0134929..149adfded 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -356,9 +356,11 @@ export async function revalidateIfRequired( * @__PURE__ */ export function fixISRHeaders(headers: OutgoingHttpHeaders) { - const regex = /s-maxage=\d+/; + const sMaxAgeRegex = /s-maxage=(\d+)/; + const match = headers[CommonHeaders.CACHE_CONTROL]?.match(sMaxAgeRegex); + const sMaxAge = match ? Number.parseInt(match[1]) : undefined; // We only apply the fix if the cache-control header contains s-maxage - if (!headers[CommonHeaders.CACHE_CONTROL]?.match(regex)) { + if (!sMaxAge) { return; } if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") { @@ -368,17 +370,17 @@ export function fixISRHeaders(headers: OutgoingHttpHeaders) { } const _lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0; if (headers[CommonHeaders.NEXT_CACHE] === "HIT" && _lastModified > 0) { - // calculate age - const age = Math.round((Date.now() - _lastModified) / 1000); - // extract s-maxage from cache-control - const cacheControl = headers[CommonHeaders.CACHE_CONTROL]; - debug("cache-control", cacheControl, _lastModified, Date.now()); - if (typeof cacheControl !== "string") return; - const match = cacheControl.match(regex); - const sMaxAge = match ? Number.parseInt(match[1]) : undefined; + debug( + "cache-control", + headers[CommonHeaders.CACHE_CONTROL], + _lastModified, + Date.now(), + ); // 31536000 is the default s-maxage value for SSG pages if (sMaxAge && sMaxAge !== 31536000) { + // calculate age + const age = Math.round((Date.now() - _lastModified) / 1000); const remainingTtl = Math.max(sMaxAge - age, 1); headers[CommonHeaders.CACHE_CONTROL] = `s-maxage=${remainingTtl}, stale-while-revalidate=2592000`;