Skip to content
This repository was archived by the owner on Jan 28, 2025. It is now read-only.

Commit 8a0d5ff

Browse files
committed
revalidation relies on the expires header
1 parent fc8bd38 commit 8a0d5ff

File tree

4 files changed

+123
-71
lines changed

4 files changed

+123
-71
lines changed

packages/libs/lambda-at-edge/src/default-handler.ts

+12-27
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { removeBlacklistedHeaders } from "./headers/removeBlacklistedHeaders";
4747
import { getStaticRegenerationResponse } from "./lib/getStaticRegenerationResponse";
4848
import { s3BucketNameFromEventRequest } from "./s3/s3BucketNameFromEventRequest";
4949
import { triggerStaticRegeneration } from "./lib/triggerStaticRegeneration";
50+
import { s3StorePage } from "./s3/s3StorePage";
5051

5152
const basePath = RoutesManifestJson.basePath;
5253

@@ -617,6 +618,7 @@ const handleOriginResponse = async ({
617618
const staticRegenerationResponse = getStaticRegenerationResponse({
618619
requestedOriginUri: uri,
619620
expiresHeader: response.headers.expires?.[0]?.value || "",
621+
lastModifiedHeader: response.headers["last-modified"]?.[0]?.value || "",
620622
manifest
621623
});
622624

@@ -694,33 +696,16 @@ const handleOriginResponse = async ({
694696
"passthrough"
695697
);
696698
if (isSSG) {
697-
const baseKey = uri
698-
.replace(/^\//, "")
699-
.replace(/\.(json|html)$/, "")
700-
.replace(/^_next\/data\/[^\/]*\//, "");
701-
const jsonKey = `_next/data/${manifest.buildId}/${baseKey}.json`;
702-
const htmlKey = `static-pages/${manifest.buildId}/${baseKey}.html`;
703-
const s3JsonParams = {
704-
Bucket: bucketName,
705-
Key: `${s3BasePath}${jsonKey}`,
706-
Body: JSON.stringify(renderOpts.pageData),
707-
ContentType: "application/json",
708-
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
709-
};
710-
const s3HtmlParams = {
711-
Bucket: bucketName,
712-
Key: `${s3BasePath}${htmlKey}`,
713-
Body: html,
714-
ContentType: "text/html",
715-
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
716-
};
717-
const { PutObjectCommand } = await import(
718-
"@aws-sdk/client-s3/commands/PutObjectCommand"
719-
);
720-
await Promise.all([
721-
s3.send(new PutObjectCommand(s3JsonParams)),
722-
s3.send(new PutObjectCommand(s3HtmlParams))
723-
]);
699+
await s3StorePage({
700+
html,
701+
uri,
702+
basePath,
703+
bucketName: bucketName || "",
704+
buildId: manifest.buildId,
705+
pageData: renderOpts.pageData,
706+
region: request.origin?.s3?.region || "",
707+
revalidate: renderOpts.revalidate
708+
});
724709
}
725710
const outHeaders: OutgoingHttpHeaders = {};
726711
Object.entries(response.headers).map(([name, headers]) => {

packages/libs/lambda-at-edge/src/lib/getStaticRegenerationResponse.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface StaticRegenerationResponseOptions {
55
requestedOriginUri: string;
66
// Header as set on the origin object
77
expiresHeader: string;
8+
lastModifiedHeader: string;
89
manifest: OriginRequestDefaultHandlerManifest;
910
}
1011

@@ -14,6 +15,15 @@ interface StaticRegenerationResponseValue {
1415
secondsRemainingUntilRevalidation: number;
1516
}
1617

18+
const firstRegenerateExpiryDate = (
19+
lastModifiedHeader: string,
20+
initialRevalidateSeconds: number
21+
) => {
22+
return new Date(
23+
new Date(lastModifiedHeader).getTime() + initialRevalidateSeconds * 1000
24+
);
25+
};
26+
1727
/**
1828
* Function called within an origin response as part of the Incremental Static
1929
* Regeneration logic. Returns required headers for the response, or false if
@@ -27,13 +37,26 @@ const getStaticRegenerationResponse = (
2737
options.requestedOriginUri.replace(".html", "")
2838
]?.initialRevalidateSeconds;
2939

30-
// If this page did not write a revalidate value at build time it is not an
31-
// ISR page
32-
if (typeof initialRevalidateSeconds !== "number") {
40+
// ISR pages that were either previously regenerated or generated
41+
// post-initial-build, will have an `Expires` header set. However ISR pages
42+
// that have not been regenerated but at build-time resolved a revalidate
43+
// property will not have an `Expires` header and therefore we check using the
44+
// manifest.
45+
if (
46+
!options.expiresHeader &&
47+
!(
48+
options.lastModifiedHeader && typeof initialRevalidateSeconds === "number"
49+
)
50+
) {
3351
return false;
3452
}
3553

36-
const expiresAt = new Date(options.expiresHeader);
54+
const expiresAt = options.expiresHeader
55+
? new Date(options.expiresHeader)
56+
: firstRegenerateExpiryDate(
57+
options.lastModifiedHeader,
58+
initialRevalidateSeconds as number
59+
);
3760

3861
// isNaN will resolve true on initial load of this page (as the expiresHeader
3962
// won't be set), in which case we trigger a regeneration now
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import lambdaAtEdgeCompat from "@sls-next/next-aws-cloudfront";
22
import { OriginRequestDefaultHandlerManifest } from "./types";
3-
import { S3Client } from "@aws-sdk/client-s3";
4-
import { buildS3RetryStrategy } from "./s3/s3RetryStrategy";
3+
import { s3StorePage } from "./s3/s3StorePage";
54

65
export const handler: AWSLambda.SQSHandler = async (event) => {
76
await Promise.all(
@@ -29,12 +28,6 @@ export const handler: AWSLambda.SQSHandler = async (event) => {
2928
manifestString
3029
);
3130

32-
const s3 = new S3Client({
33-
region: cloudFrontEventRequest.origin?.s3?.region,
34-
maxAttempts: 3,
35-
retryStrategy: await buildS3RetryStrategy()
36-
});
37-
3831
const { req, res } = lambdaAtEdgeCompat(
3932
{ request: cloudFrontEventRequest },
4033
{ enableHTTPCompression: manifest.enableHTTPCompression }
@@ -50,44 +43,22 @@ export const handler: AWSLambda.SQSHandler = async (event) => {
5043
// eslint-disable-next-line @typescript-eslint/no-var-requires
5144
const page = require(`./pages${srcPath}`);
5245

53-
const jsonKey = `_next/data/${manifest.buildId}${baseKey}.json`;
54-
const htmlKey = `static-pages/${manifest.buildId}${baseKey}.html`;
55-
5646
const { renderOpts, html } = await page.renderReqToHTML(
5747
req,
5848
res,
5949
"passthrough"
6050
);
6151

62-
const revalidate =
63-
renderOpts.revalidate ?? ssgRoute.initialRevalidateSeconds;
64-
const expires = new Date(Date.now() + revalidate * 1000);
65-
const s3BasePath = basePath ? `${basePath.replace(/^\//, "")}/` : "";
66-
const s3JsonParams = {
67-
Bucket: bucketName,
68-
Key: `${s3BasePath}${jsonKey}`,
69-
Body: JSON.stringify(renderOpts.pageData),
70-
ContentType: "application/json",
71-
Expires: expires,
72-
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
73-
};
74-
75-
const s3HtmlParams = {
76-
Bucket: bucketName,
77-
Key: `${s3BasePath}${htmlKey}`,
78-
Body: html,
79-
ContentType: "text/html",
80-
Expires: expires,
81-
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
82-
};
83-
84-
const { PutObjectCommand } = await import(
85-
"@aws-sdk/client-s3/commands/PutObjectCommand"
86-
);
87-
await Promise.all([
88-
s3.send(new PutObjectCommand(s3JsonParams)),
89-
s3.send(new PutObjectCommand(s3HtmlParams))
90-
]);
52+
await s3StorePage({
53+
html,
54+
uri: cloudFrontEventRequest.uri,
55+
basePath,
56+
bucketName: bucketName || "",
57+
buildId: manifest.buildId,
58+
pageData: renderOpts.pageData,
59+
region: cloudFrontEventRequest.origin?.s3?.region || "",
60+
revalidate: renderOpts.revalidate
61+
});
9162
})
9263
);
9364
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { buildS3RetryStrategy } from "./s3RetryStrategy";
2+
3+
interface S3StorePageOptions {
4+
basePath: string | undefined;
5+
uri: string;
6+
revalidate?: number | undefined;
7+
bucketName: string;
8+
html: string;
9+
buildId: string;
10+
region: string;
11+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12+
pageData: Record<string, any>;
13+
}
14+
15+
/**
16+
* There are multiple occasions where a static/SSG page will be generated after
17+
* the initial build. This function accepts a generated page, stores it and
18+
* applies the appropriate headers (e.g. setting an 'Expires' header for
19+
* regeneration).
20+
*/
21+
export const s3StorePage = async (
22+
options: S3StorePageOptions
23+
): Promise<void> => {
24+
const { S3Client } = await import("@aws-sdk/client-s3/S3Client");
25+
26+
const s3 = new S3Client({
27+
region: options.region,
28+
maxAttempts: 3,
29+
retryStrategy: await buildS3RetryStrategy()
30+
});
31+
32+
const s3BasePath = options.basePath
33+
? `${options.basePath.replace(/^\//, "")}/`
34+
: "";
35+
const baseKey = options.uri
36+
.replace(/^\//, "")
37+
.replace(/\.(json|html)$/, "")
38+
.replace(/^_next\/data\/[^\/]*\//, "");
39+
const jsonKey = `_next/data/${options.buildId}/${baseKey}.json`;
40+
const htmlKey = `static-pages/${options.buildId}/${baseKey}.html`;
41+
const cacheControl = options.revalidate
42+
? undefined
43+
: "public, max-age=0, s-maxage=2678400, must-revalidate";
44+
const expires = options.revalidate
45+
? new Date(new Date().getTime() + 1000 * options.revalidate)
46+
: undefined;
47+
48+
const s3JsonParams = {
49+
Bucket: options.bucketName,
50+
Key: `${s3BasePath}${jsonKey}`,
51+
Body: JSON.stringify(options.pageData),
52+
ContentType: "application/json",
53+
CacheControl: cacheControl,
54+
Expires: expires
55+
};
56+
57+
const s3HtmlParams = {
58+
Bucket: options.bucketName,
59+
Key: `${s3BasePath}${htmlKey}`,
60+
Body: options.html,
61+
ContentType: "text/html",
62+
CacheControl: cacheControl,
63+
Expires: expires
64+
};
65+
66+
const { PutObjectCommand } = await import(
67+
"@aws-sdk/client-s3/commands/PutObjectCommand"
68+
);
69+
await Promise.all([
70+
s3.send(new PutObjectCommand(s3JsonParams)),
71+
s3.send(new PutObjectCommand(s3HtmlParams))
72+
]);
73+
};

0 commit comments

Comments
 (0)