Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix swr client inconsistencies #289

Merged
merged 6 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/lazy-pens-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"open-next": minor
---

Fix inconsistencies with swr and isr (#289)

Exclude manifest.json, robots.txt and sitemap.xml from routing matcher (#287)

Feature/rewrite with query string (#281)

Double chunk DDB batch writes to not overwhelm DDB on load (#293)

fix: copy favicon.ico from app dir (#301)

fix: XML Malformed Error DeleteObjectsCommand (#300)

Fix external rewrite (#299)

Perf Reduce s3 calls (#295)
2 changes: 2 additions & 0 deletions packages/open-next/src/adapters/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
var dynamoClient: DynamoDBClient;
var disableDynamoDBCache: boolean;
var disableIncrementalCache: boolean;
var lastModified: number;
}

export default class S3Cache {
Expand Down Expand Up @@ -201,6 +202,7 @@
// If some tags are stale we need to force revalidation
return null;
}
globalThis.lastModified = lastModified;
if (cacheData.type === "route") {
return {
lastModified: LastModified?.getTime(),
Expand Down Expand Up @@ -285,7 +287,7 @@
props: data.props,
} as S3CachedFile),
);
} else if (data === null || data === undefined) {

Check warning on line 290 in packages/open-next/src/adapters/cache.ts

View workflow job for this annotation

GitHub Actions / validate

Add the missing "else" clause
await this.deleteS3Objects(key);
}
// Write derivedTags to dynamodb
Expand Down
124 changes: 70 additions & 54 deletions packages/open-next/src/adapters/plugins/routing/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { awsLogger, debug } from "../../logger.js";
declare global {
var openNextDebug: boolean;
var openNextVersion: string;
var lastModified: number;
}

enum CommonHeaders {
CACHE_CONTROL = "cache-control",
NEXT_CACHE = "x-nextjs-cache",
}

// Expected environment variables
Expand Down Expand Up @@ -105,58 +107,50 @@ export async function revalidateIfRequired(
headers: Record<string, string | undefined>,
req?: IncomingMessage,
) {
// 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
if (headers["x-nextjs-cache"] === "REVALIDATED") {
headers[CommonHeaders.CACHE_CONTROL] =
"private, no-cache, no-store, max-age=0, must-revalidate";
return;
}
if (headers["x-nextjs-cache"] !== "STALE") return;

// If the cache is stale, we revalidate in the background
// In order for CloudFront SWR to work, we set the stale-while-revalidate value to 2 seconds
// This will cause CloudFront to cache the stale data for a short period of time while we revalidate in the background
// Once the revalidation is complete, CloudFront will serve the fresh data
headers[CommonHeaders.CACHE_CONTROL] =
"s-maxage=2, stale-while-revalidate=2592000";

// If the URL is rewritten, revalidation needs to be done on the rewritten URL.
// - Link to Next.js doc: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation
// - Link to NextInternalRequestMeta: https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/request-meta.ts#L11
// @ts-ignore
const internalMeta = req?.[Symbol.for("NextInternalRequestMeta")];

// When using Pages Router, two requests will be received:
// 1. one for the page: /foo
// 2. one for the json data: /_next/data/BUILD_ID/foo.json
// The rewritten url is correct for 1, but that for the second request
// does not include the "/_next/data/" prefix. Need to add it.
const revalidateUrl = internalMeta?._nextDidRewrite
? rawPath.startsWith("/_next/data/")
? `/_next/data/${BuildId}${internalMeta?._nextRewroteUrl}.json`
: internalMeta?._nextRewroteUrl
: rawPath;

// We need to pass etag to the revalidation queue to try to bypass the default 5 min deduplication window.
// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html
// If you need to have a revalidation happen more frequently than 5 minutes,
// your page will need to have a different etag to bypass the deduplication window.
// If data has the same etag during these 5 min dedup window, it will be deduplicated and not revalidated.
try {
const hash = (str: string) =>
crypto.createHash("md5").update(str).digest("hex");

await sqsClient.send(
new SendMessageCommand({
QueueUrl: REVALIDATION_QUEUE_URL,
MessageDeduplicationId: hash(`${rawPath}-${headers.etag}`),
MessageBody: JSON.stringify({ host, url: revalidateUrl }),
MessageGroupId: generateMessageGroupId(rawPath),
}),
);
} catch (e) {
debug(`Failed to revalidate stale page ${rawPath}`);
debug(e);
fixISRHeaders(headers);

if (headers[CommonHeaders.NEXT_CACHE] === "STALE") {
// If the URL is rewritten, revalidation needs to be done on the rewritten URL.
// - Link to Next.js doc: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation
// - Link to NextInternalRequestMeta: https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/request-meta.ts#L11
// @ts-ignore
const internalMeta = req?.[Symbol.for("NextInternalRequestMeta")];

// When using Pages Router, two requests will be received:
// 1. one for the page: /foo
// 2. one for the json data: /_next/data/BUILD_ID/foo.json
// The rewritten url is correct for 1, but that for the second request
// does not include the "/_next/data/" prefix. Need to add it.
const revalidateUrl = internalMeta?._nextDidRewrite
? rawPath.startsWith("/_next/data/")
? `/_next/data/${BuildId}${internalMeta?._nextRewroteUrl}.json`
: internalMeta?._nextRewroteUrl
: rawPath;

// We need to pass etag to the revalidation queue to try to bypass the default 5 min deduplication window.
// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/using-messagededuplicationid-property.html
// If you need to have a revalidation happen more frequently than 5 minutes,
// your page will need to have a different etag to bypass the deduplication window.
// If data has the same etag during these 5 min dedup window, it will be deduplicated and not revalidated.
try {
const hash = (str: string) =>
crypto.createHash("md5").update(str).digest("hex");

const lastModified =
globalThis.lastModified > 0 ? globalThis.lastModified : "";

await sqsClient.send(
new SendMessageCommand({
QueueUrl: REVALIDATION_QUEUE_URL,
MessageDeduplicationId: hash(`${rawPath}-${lastModified}`),
MessageBody: JSON.stringify({ host, url: revalidateUrl }),
MessageGroupId: generateMessageGroupId(rawPath),
}),
);
} catch (e) {
debug(`Failed to revalidate stale page ${rawPath}`);
debug(e);
}
}
}

Expand Down Expand Up @@ -205,12 +199,34 @@ function cyrb128(str: string) {
}

export function fixISRHeaders(headers: Record<string, string | undefined>) {
if (headers["x-nextjs-cache"] === "REVALIDATED") {
if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
headers[CommonHeaders.CACHE_CONTROL] =
"private, no-cache, no-store, max-age=0, must-revalidate";
return;
}
if (headers["x-nextjs-cache"] !== "STALE") return;
if (
headers[CommonHeaders.NEXT_CACHE] === "HIT" &&
globalThis.lastModified > 0
) {
// calculate age
const age = Math.round((Date.now() - globalThis.lastModified) / 1000);
// extract s-maxage from cache-control
const regex = /s-maxage=(\d+)/;
const match = headers[CommonHeaders.CACHE_CONTROL]?.match(regex);
const sMaxAge = match ? parseInt(match[1]) : undefined;

// 31536000 is the default s-maxage value for SSG pages
if (sMaxAge && sMaxAge !== 31536000) {
const remainingTtl = Math.max(sMaxAge - age, 1);
headers[
CommonHeaders.CACHE_CONTROL
] = `s-maxage=${remainingTtl}, stale-while-revalidate=2592000`;
}

// reset lastModified
globalThis.lastModified = 0;
}
if (headers[CommonHeaders.NEXT_CACHE] !== "STALE") return;

// If the cache is stale, we revalidate in the background
// In order for CloudFront SWR to work, we set the stale-while-revalidate value to 2 seconds
Expand Down
2 changes: 2 additions & 0 deletions packages/open-next/src/adapters/routing/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export async function handleMiddleware(
const { rawPath, query } = internalEvent;
const hasMatch = middleMatch.some((r) => r.test(rawPath));
if (!hasMatch) return internalEvent;
// We bypass the middleware if the request is internal
if (internalEvent.headers["x-isr"]) return internalEvent;

const req = new IncomingMessage(internalEvent);
const res = new ServerlessResponse({
Expand Down
Loading