Skip to content

Commit e821f51

Browse files
conico974vicb
andauthored
Optimize header fixing logic and improve tests (#1036)
* Apply fixHeaders only once * Don't fix cache-control headers if they've been changed * update test * linting * changeset * fix unit test * Update packages/open-next/src/core/routing/util.ts Co-authored-by: Victor Berchet <[email protected]> * fixup! refactor --------- Co-authored-by: Victor Berchet <[email protected]>
1 parent 01f7cdc commit e821f51

File tree

5 files changed

+65
-11
lines changed

5 files changed

+65
-11
lines changed

.changeset/large-pants-press.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
Fix cache-control header set in middleware being overriden for ISR route

packages/open-next/src/core/routing/util.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -356,25 +356,31 @@ export async function revalidateIfRequired(
356356
* @__PURE__
357357
*/
358358
export function fixISRHeaders(headers: OutgoingHttpHeaders) {
359+
const sMaxAgeRegex = /s-maxage=(\d+)/;
360+
const match = headers[CommonHeaders.CACHE_CONTROL]?.match(sMaxAgeRegex);
361+
const sMaxAge = match ? Number.parseInt(match[1]) : undefined;
362+
// We only apply the fix if the cache-control header contains s-maxage
363+
if (!sMaxAge) {
364+
return;
365+
}
359366
if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
360367
headers[CommonHeaders.CACHE_CONTROL] =
361368
"private, no-cache, no-store, max-age=0, must-revalidate";
362369
return;
363370
}
364371
const _lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0;
365372
if (headers[CommonHeaders.NEXT_CACHE] === "HIT" && _lastModified > 0) {
366-
// calculate age
367-
const age = Math.round((Date.now() - _lastModified) / 1000);
368-
// extract s-maxage from cache-control
369-
const regex = /s-maxage=(\d+)/;
370-
const cacheControl = headers[CommonHeaders.CACHE_CONTROL];
371-
debug("cache-control", cacheControl, _lastModified, Date.now());
372-
if (typeof cacheControl !== "string") return;
373-
const match = cacheControl.match(regex);
374-
const sMaxAge = match ? Number.parseInt(match[1]) : undefined;
373+
debug(
374+
"cache-control",
375+
headers[CommonHeaders.CACHE_CONTROL],
376+
_lastModified,
377+
Date.now(),
378+
);
375379

376380
// 31536000 is the default s-maxage value for SSG pages
377381
if (sMaxAge && sMaxAge !== 31536000) {
382+
// calculate age
383+
const age = Math.round((Date.now() - _lastModified) / 1000);
378384
const remainingTtl = Math.max(sMaxAge - age, 1);
379385
headers[CommonHeaders.CACHE_CONTROL] =
380386
`s-maxage=${remainingTtl}, stale-while-revalidate=2592000`;

packages/open-next/src/http/openNextResponse.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse {
2222
headers: OutgoingHttpHeaders = {};
2323
headersSent = false;
2424
_chunks: Buffer[] = [];
25+
headersAlreadyFixed = false;
2526

2627
private _cookies: string[] = [];
2728
private responseStream?: Writable;
@@ -69,7 +70,7 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse {
6970
}
7071

7172
constructor(
72-
private fixHeaders: (headers: OutgoingHttpHeaders) => void,
73+
private fixHeadersFn: (headers: OutgoingHttpHeaders) => void,
7374
private onEnd: (headers: OutgoingHttpHeaders) => Promise<void>,
7475
private streamCreator?: StreamCreator,
7576
private initialHeaders?: OutgoingHttpHeaders,
@@ -271,6 +272,14 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse {
271272
* OpenNext specific method
272273
*/
273274

275+
fixHeaders(headers: OutgoingHttpHeaders) {
276+
if (this.headersAlreadyFixed) {
277+
return;
278+
}
279+
this.fixHeadersFn(headers);
280+
this.headersAlreadyFixed = true;
281+
}
282+
274283
getFixedHeaders(): OutgoingHttpHeaders {
275284
// Do we want to apply this on writeHead?
276285
this.fixHeaders(this.headers);

packages/tests-e2e/tests/appRouter/isr.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,26 @@ test("headers", async ({ page }) => {
4545
return response.status() === 200;
4646
});
4747
await page.goto("/isr");
48+
let hasBeenStale = false;
49+
let hasBeenHit = false;
4850

4951
while (true) {
5052
const response = await responsePromise;
5153
const headers = response.headers();
54+
expect(headers["cache-control"]).toBe(
55+
"max-age=10, stale-while-revalidate=999",
56+
);
57+
const cacheHeader =
58+
headers["x-nextjs-cache"] ?? headers["x-opennext-cache"];
59+
if (cacheHeader === "STALE") {
60+
hasBeenStale = true;
61+
}
62+
if (cacheHeader === "HIT") {
63+
hasBeenHit = true;
64+
}
5265

5366
// this was set in middleware
54-
if (headers["cache-control"] === "max-age=10, stale-while-revalidate=999") {
67+
if (hasBeenStale && hasBeenHit) {
5568
break;
5669
}
5770
await page.waitForTimeout(1000);

packages/tests-unit/tests/core/routing/util.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,7 @@ describe("fixISRHeaders", () => {
748748
it("should set cache-control directive to must-revalidate when x-nextjs-cache is REVALIDATED", () => {
749749
const headers: Record<string, string> = {
750750
"x-nextjs-cache": "REVALIDATED",
751+
"cache-control": "s-maxage=10, stale-while-revalidate=86400",
751752
};
752753
fixISRHeaders(headers);
753754

@@ -771,13 +772,33 @@ describe("fixISRHeaders", () => {
771772
it("should set cache-control directive to stale-while-revalidate when x-nextjs-cache is STALE", () => {
772773
const headers: Record<string, string> = {
773774
"x-nextjs-cache": "STALE",
775+
"cache-control": "s-maxage=86400", // 1 day
774776
};
775777
fixISRHeaders(headers);
776778

777779
expect(headers["cache-control"]).toBe(
778780
"s-maxage=2, stale-while-revalidate=2592000",
779781
);
780782
});
783+
784+
it("should not modify cache-control when cache-control is missing", () => {
785+
const headers: Record<string, string> = {
786+
"x-nextjs-cache": "HIT",
787+
};
788+
fixISRHeaders(headers);
789+
790+
expect(headers["cache-control"]).toBeUndefined();
791+
});
792+
793+
it("should not modify cache-control when cache-control has no s-maxage", () => {
794+
const headers: Record<string, string> = {
795+
"x-nextjs-cache": "HIT",
796+
"cache-control": "private, max-age=0",
797+
};
798+
fixISRHeaders(headers);
799+
800+
expect(headers["cache-control"]).toBe("private, max-age=0");
801+
});
781802
});
782803

783804
describe("invalidateCDNOnRequest", () => {

0 commit comments

Comments
 (0)