Skip to content

Commit 22e3e47

Browse files
authored
Fix swr client inconsistencies (#289)
* fix inconsistencies between rsc or json and html in the cdn * fix 5 minute min revalidate * exclude ssg pages * only apply revalidate for stale page * bypass middleware for isr * add changeset
1 parent b7c8f47 commit 22e3e47

File tree

4 files changed

+93
-54
lines changed

4 files changed

+93
-54
lines changed

.changeset/lazy-pens-bake.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"open-next": minor
3+
---
4+
5+
Fix inconsistencies with swr and isr (#289)
6+
7+
Exclude manifest.json, robots.txt and sitemap.xml from routing matcher (#287)
8+
9+
Feature/rewrite with query string (#281)
10+
11+
Double chunk DDB batch writes to not overwhelm DDB on load (#293)
12+
13+
fix: copy favicon.ico from app dir (#301)
14+
15+
fix: XML Malformed Error DeleteObjectsCommand (#300)
16+
17+
Fix external rewrite (#299)
18+
19+
Perf Reduce s3 calls (#295)

packages/open-next/src/adapters/cache.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ declare global {
137137
var dynamoClient: DynamoDBClient;
138138
var disableDynamoDBCache: boolean;
139139
var disableIncrementalCache: boolean;
140+
var lastModified: number;
140141
}
141142

142143
export default class S3Cache {
@@ -201,6 +202,7 @@ export default class S3Cache {
201202
// If some tags are stale we need to force revalidation
202203
return null;
203204
}
205+
globalThis.lastModified = lastModified;
204206
if (cacheData.type === "route") {
205207
return {
206208
lastModified: LastModified?.getTime(),

packages/open-next/src/adapters/plugins/routing/util.ts

Lines changed: 70 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import { awsLogger, debug } from "../../logger.js";
1010
declare global {
1111
var openNextDebug: boolean;
1212
var openNextVersion: string;
13+
var lastModified: number;
1314
}
1415

1516
enum CommonHeaders {
1617
CACHE_CONTROL = "cache-control",
18+
NEXT_CACHE = "x-nextjs-cache",
1719
}
1820

1921
// Expected environment variables
@@ -105,58 +107,50 @@ export async function revalidateIfRequired(
105107
headers: Record<string, string | undefined>,
106108
req?: IncomingMessage,
107109
) {
108-
// If the page has been revalidated via on demand revalidation, we need to remove the cache-control so that CloudFront doesn't cache the page
109-
if (headers["x-nextjs-cache"] === "REVALIDATED") {
110-
headers[CommonHeaders.CACHE_CONTROL] =
111-
"private, no-cache, no-store, max-age=0, must-revalidate";
112-
return;
113-
}
114-
if (headers["x-nextjs-cache"] !== "STALE") return;
115-
116-
// If the cache is stale, we revalidate in the background
117-
// In order for CloudFront SWR to work, we set the stale-while-revalidate value to 2 seconds
118-
// This will cause CloudFront to cache the stale data for a short period of time while we revalidate in the background
119-
// Once the revalidation is complete, CloudFront will serve the fresh data
120-
headers[CommonHeaders.CACHE_CONTROL] =
121-
"s-maxage=2, stale-while-revalidate=2592000";
122-
123-
// If the URL is rewritten, revalidation needs to be done on the rewritten URL.
124-
// - Link to Next.js doc: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation
125-
// - Link to NextInternalRequestMeta: https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/request-meta.ts#L11
126-
// @ts-ignore
127-
const internalMeta = req?.[Symbol.for("NextInternalRequestMeta")];
128-
129-
// When using Pages Router, two requests will be received:
130-
// 1. one for the page: /foo
131-
// 2. one for the json data: /_next/data/BUILD_ID/foo.json
132-
// The rewritten url is correct for 1, but that for the second request
133-
// does not include the "/_next/data/" prefix. Need to add it.
134-
const revalidateUrl = internalMeta?._nextDidRewrite
135-
? rawPath.startsWith("/_next/data/")
136-
? `/_next/data/${BuildId}${internalMeta?._nextRewroteUrl}.json`
137-
: internalMeta?._nextRewroteUrl
138-
: rawPath;
139-
140-
// We need to pass etag to the revalidation queue to try to bypass the default 5 min deduplication window.
141-
// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html
142-
// If you need to have a revalidation happen more frequently than 5 minutes,
143-
// your page will need to have a different etag to bypass the deduplication window.
144-
// If data has the same etag during these 5 min dedup window, it will be deduplicated and not revalidated.
145-
try {
146-
const hash = (str: string) =>
147-
crypto.createHash("md5").update(str).digest("hex");
148-
149-
await sqsClient.send(
150-
new SendMessageCommand({
151-
QueueUrl: REVALIDATION_QUEUE_URL,
152-
MessageDeduplicationId: hash(`${rawPath}-${headers.etag}`),
153-
MessageBody: JSON.stringify({ host, url: revalidateUrl }),
154-
MessageGroupId: generateMessageGroupId(rawPath),
155-
}),
156-
);
157-
} catch (e) {
158-
debug(`Failed to revalidate stale page ${rawPath}`);
159-
debug(e);
110+
fixISRHeaders(headers);
111+
112+
if (headers[CommonHeaders.NEXT_CACHE] === "STALE") {
113+
// If the URL is rewritten, revalidation needs to be done on the rewritten URL.
114+
// - Link to Next.js doc: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation
115+
// - Link to NextInternalRequestMeta: https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/request-meta.ts#L11
116+
// @ts-ignore
117+
const internalMeta = req?.[Symbol.for("NextInternalRequestMeta")];
118+
119+
// When using Pages Router, two requests will be received:
120+
// 1. one for the page: /foo
121+
// 2. one for the json data: /_next/data/BUILD_ID/foo.json
122+
// The rewritten url is correct for 1, but that for the second request
123+
// does not include the "/_next/data/" prefix. Need to add it.
124+
const revalidateUrl = internalMeta?._nextDidRewrite
125+
? rawPath.startsWith("/_next/data/")
126+
? `/_next/data/${BuildId}${internalMeta?._nextRewroteUrl}.json`
127+
: internalMeta?._nextRewroteUrl
128+
: rawPath;
129+
130+
// We need to pass etag to the revalidation queue to try to bypass the default 5 min deduplication window.
131+
// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html
132+
// If you need to have a revalidation happen more frequently than 5 minutes,
133+
// your page will need to have a different etag to bypass the deduplication window.
134+
// If data has the same etag during these 5 min dedup window, it will be deduplicated and not revalidated.
135+
try {
136+
const hash = (str: string) =>
137+
crypto.createHash("md5").update(str).digest("hex");
138+
139+
const lastModified =
140+
globalThis.lastModified > 0 ? globalThis.lastModified : "";
141+
142+
await sqsClient.send(
143+
new SendMessageCommand({
144+
QueueUrl: REVALIDATION_QUEUE_URL,
145+
MessageDeduplicationId: hash(`${rawPath}-${lastModified}`),
146+
MessageBody: JSON.stringify({ host, url: revalidateUrl }),
147+
MessageGroupId: generateMessageGroupId(rawPath),
148+
}),
149+
);
150+
} catch (e) {
151+
debug(`Failed to revalidate stale page ${rawPath}`);
152+
debug(e);
153+
}
160154
}
161155
}
162156

@@ -205,12 +199,34 @@ function cyrb128(str: string) {
205199
}
206200

207201
export function fixISRHeaders(headers: Record<string, string | undefined>) {
208-
if (headers["x-nextjs-cache"] === "REVALIDATED") {
202+
if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
209203
headers[CommonHeaders.CACHE_CONTROL] =
210204
"private, no-cache, no-store, max-age=0, must-revalidate";
211205
return;
212206
}
213-
if (headers["x-nextjs-cache"] !== "STALE") return;
207+
if (
208+
headers[CommonHeaders.NEXT_CACHE] === "HIT" &&
209+
globalThis.lastModified > 0
210+
) {
211+
// calculate age
212+
const age = Math.round((Date.now() - globalThis.lastModified) / 1000);
213+
// extract s-maxage from cache-control
214+
const regex = /s-maxage=(\d+)/;
215+
const match = headers[CommonHeaders.CACHE_CONTROL]?.match(regex);
216+
const sMaxAge = match ? parseInt(match[1]) : undefined;
217+
218+
// 31536000 is the default s-maxage value for SSG pages
219+
if (sMaxAge && sMaxAge !== 31536000) {
220+
const remainingTtl = Math.max(sMaxAge - age, 1);
221+
headers[
222+
CommonHeaders.CACHE_CONTROL
223+
] = `s-maxage=${remainingTtl}, stale-while-revalidate=2592000`;
224+
}
225+
226+
// reset lastModified
227+
globalThis.lastModified = 0;
228+
}
229+
if (headers[CommonHeaders.NEXT_CACHE] !== "STALE") return;
214230

215231
// If the cache is stale, we revalidate in the background
216232
// In order for CloudFront SWR to work, we set the stale-while-revalidate value to 2 seconds

packages/open-next/src/adapters/routing/middleware.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export async function handleMiddleware(
4444
const { rawPath, query } = internalEvent;
4545
const hasMatch = middleMatch.some((r) => r.test(rawPath));
4646
if (!hasMatch) return internalEvent;
47+
// We bypass the middleware if the request is internal
48+
if (internalEvent.headers["x-isr"]) return internalEvent;
4749

4850
const req = new IncomingMessage(internalEvent);
4951
const res = new ServerlessResponse({

0 commit comments

Comments
 (0)