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

Commit fc8bd38

Browse files
committed
use expires header rather than last-modified and a small code tidy
1 parent 6f1ce93 commit fc8bd38

File tree

8 files changed

+212
-82
lines changed

8 files changed

+212
-82
lines changed

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

Lines changed: 22 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ import {
4444
getLocalePrefixFromUri
4545
} from "./routing/locale-utils";
4646
import { removeBlacklistedHeaders } from "./headers/removeBlacklistedHeaders";
47+
import { getStaticRegenerationResponse } from "./lib/getStaticRegenerationResponse";
48+
import { s3BucketNameFromEventRequest } from "./s3/s3BucketNameFromEventRequest";
49+
import { triggerStaticRegeneration } from "./lib/triggerStaticRegeneration";
4750

4851
const basePath = RoutesManifestJson.basePath;
4952

@@ -590,7 +593,6 @@ const handleOriginRequest = async ({
590593
const handleOriginResponse = async ({
591594
event,
592595
manifest,
593-
prerenderManifest,
594596
routesManifest
595597
}: {
596598
event: OriginResponseEvent;
@@ -602,8 +604,7 @@ const handleOriginResponse = async ({
602604
const request = event.Records[0].cf.request;
603605
const { uri } = request;
604606
const { status } = response;
605-
const { region, domainName } = request.origin?.s3 || {};
606-
const bucketName = domainName?.replace(`.s3.${region}.amazonaws.com`, "");
607+
const bucketName = s3BucketNameFromEventRequest(request);
607608

608609
if (status !== "403") {
609610
// Set 404 status code for 404.html page. We do not need normalised URI as it will always be "/404.html"
@@ -613,83 +614,32 @@ const handleOriginResponse = async ({
613614
return response;
614615
}
615616

616-
const initialRevalidateSeconds =
617-
manifest.pages.ssg.nonDynamic?.[uri.replace(".html", "")]
618-
?.initialRevalidateSeconds;
619-
const lastModifiedHeaderString =
620-
response.headers?.["last-modified"]?.[0]?.value;
621-
const lastModifiedAt = lastModifiedHeaderString
622-
? new Date(lastModifiedHeaderString)
623-
: null;
624-
if (typeof initialRevalidateSeconds === "number" && lastModifiedAt) {
625-
/**
626-
* TODO: Refactor to use the returned `Expired` header.
627-
*/
628-
const createdAgo =
629-
(Date.now() - (lastModifiedAt.getTime() || Date.now())) / 1000;
630-
631-
const timeToRevalidate = Math.floor(
632-
initialRevalidateSeconds - createdAgo
633-
);
617+
const staticRegenerationResponse = getStaticRegenerationResponse({
618+
requestedOriginUri: uri,
619+
expiresHeader: response.headers.expires?.[0]?.value || "",
620+
manifest
621+
});
634622

623+
if (staticRegenerationResponse) {
635624
response.headers["cache-control"] = [
636625
{
637626
key: "Cache-Control",
638-
value:
639-
timeToRevalidate < 0
640-
? "public, max-age=0, s-maxage=0, must-revalidate"
641-
: `public, max-age=0, s-maxage=${timeToRevalidate}, must-revalidate`
627+
value: staticRegenerationResponse.cacheControl
642628
}
643629
];
644630

645-
if (timeToRevalidate < 0) {
646-
const { SQSClient, SendMessageCommand } = await import(
647-
"@aws-sdk/client-sqs"
648-
);
649-
const sqs = new SQSClient({
650-
region,
651-
maxAttempts: 3,
652-
retryStrategy: await buildS3RetryStrategy()
631+
// We don't want the `expires` header to be sent to the client we manage
632+
// the cache at the edge using the s-maxage directive in the cache-control
633+
// header
634+
delete response.headers.expires;
635+
636+
if (staticRegenerationResponse.secondsRemainingUntilRevalidation === 0) {
637+
await triggerStaticRegeneration({
638+
basePath,
639+
manifest,
640+
request,
641+
response
653642
});
654-
await sqs.send(
655-
new SendMessageCommand({
656-
QueueUrl: `https://sqs.${region}.amazonaws.com/${bucketName}.fifo`,
657-
MessageBody: uri,
658-
MessageAttributes: {
659-
BucketRegion: {
660-
DataType: "String",
661-
StringValue: region
662-
},
663-
BucketName: {
664-
DataType: "String",
665-
StringValue: bucketName
666-
},
667-
CloudFrontEventRequest: {
668-
DataType: "String",
669-
StringValue: JSON.stringify(request)
670-
},
671-
Manifest: {
672-
DataType: "String",
673-
StringValue: JSON.stringify(manifest)
674-
},
675-
...(basePath
676-
? {
677-
BasePath: {
678-
DataType: "String",
679-
StringValue: basePath
680-
}
681-
}
682-
: {})
683-
},
684-
// We only want to trigger the regeneration once for every previous
685-
// update. This will prevent the case where this page is being
686-
// requested again whilst its already started to regenerate.
687-
MessageDeduplicationId: lastModifiedAt.getTime().toString(),
688-
// Only deduplicate based on the object, i.e. we can generate
689-
// different pages in parallel, just not the same one
690-
MessageGroupId: uri
691-
})
692-
);
693643
}
694644
}
695645

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "./routing/redirector";
2121
import { getUnauthenticatedResponse } from "./auth/authenticator";
2222
import { removeBlacklistedHeaders } from "./headers/removeBlacklistedHeaders";
23+
import { s3BucketNameFromEventRequest } from "./s3/s3BucketNameFromEventRequest";
2324

2425
const basePath = RoutesManifestJson.basePath;
2526

@@ -88,11 +89,11 @@ export const handler = async (
8889
true
8990
);
9091

91-
const { domainName, region } = request.origin!.s3!;
92-
const bucketName = domainName.replace(`.s3.${region}.amazonaws.com`, "");
92+
const { region } = request.origin!.s3!;
93+
const bucketName = s3BucketNameFromEventRequest(request);
9394

9495
await imageOptimizer(
95-
{ basePath: basePath, bucketName: bucketName, region: region },
96+
{ basePath: basePath, bucketName: bucketName || "", region: region },
9697
imagesManifest,
9798
req,
9899
res,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { OriginRequestDefaultHandlerManifest } from "../types";
2+
3+
interface StaticRegenerationResponseOptions {
4+
// URI of the origin object
5+
requestedOriginUri: string;
6+
// Header as set on the origin object
7+
expiresHeader: string;
8+
manifest: OriginRequestDefaultHandlerManifest;
9+
}
10+
11+
interface StaticRegenerationResponseValue {
12+
// Cache-Control header
13+
cacheControl: string;
14+
secondsRemainingUntilRevalidation: number;
15+
}
16+
17+
/**
18+
* Function called within an origin response as part of the Incremental Static
19+
* Regeneration logic. Returns required headers for the response, or false if
20+
* this response is not compatible with ISR.
21+
*/
22+
const getStaticRegenerationResponse = (
23+
options: StaticRegenerationResponseOptions
24+
): StaticRegenerationResponseValue | false => {
25+
const initialRevalidateSeconds =
26+
options.manifest.pages.ssg.nonDynamic?.[
27+
options.requestedOriginUri.replace(".html", "")
28+
]?.initialRevalidateSeconds;
29+
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") {
33+
return false;
34+
}
35+
36+
const expiresAt = new Date(options.expiresHeader);
37+
38+
// isNaN will resolve true on initial load of this page (as the expiresHeader
39+
// won't be set), in which case we trigger a regeneration now
40+
const secondsRemainingUntilRevalidation = isNaN(expiresAt.getTime())
41+
? 0
42+
: // Never return a negative amount of seconds if revalidation could have
43+
// happened sooner
44+
Math.floor(Math.max(0, (expiresAt.getTime() - Date.now()) / 1000));
45+
46+
return {
47+
secondsRemainingUntilRevalidation,
48+
cacheControl: `public, max-age=0, s-maxage=${secondsRemainingUntilRevalidation}, must-revalidate`
49+
};
50+
};
51+
52+
export { getStaticRegenerationResponse };
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { s3BucketNameFromEventRequest } from "../s3/s3BucketNameFromEventRequest";
2+
import { buildS3RetryStrategy } from "../s3/s3RetryStrategy";
3+
import { OriginRequestDefaultHandlerManifest } from "../types";
4+
5+
interface TriggerStaticRegenerationOptions {
6+
request: AWSLambda.CloudFrontRequest;
7+
response: AWSLambda.CloudFrontResponse;
8+
manifest: OriginRequestDefaultHandlerManifest;
9+
basePath: string | undefined;
10+
}
11+
12+
export const triggerStaticRegeneration = async (
13+
options: TriggerStaticRegenerationOptions
14+
): Promise<void> => {
15+
const { region } = options.request.origin?.s3 || {};
16+
const bucketName = s3BucketNameFromEventRequest(options.request);
17+
18+
const { SQSClient, SendMessageCommand } = await import("@aws-sdk/client-sqs");
19+
const sqs = new SQSClient({
20+
region,
21+
maxAttempts: 3,
22+
retryStrategy: await buildS3RetryStrategy()
23+
});
24+
25+
const lastModifiedAt = new Date(
26+
options.response.headers["last-modified"]?.[0].value
27+
)
28+
.getTime()
29+
.toString();
30+
31+
await sqs.send(
32+
new SendMessageCommand({
33+
QueueUrl: `https://sqs.${region}.amazonaws.com/${bucketName}.fifo`,
34+
MessageBody: options.request.uri, // This is not used, however it is a required property
35+
MessageAttributes: {
36+
BucketRegion: {
37+
DataType: "String",
38+
StringValue: region
39+
},
40+
BucketName: {
41+
DataType: "String",
42+
StringValue: bucketName
43+
},
44+
CloudFrontEventRequest: {
45+
DataType: "String",
46+
StringValue: JSON.stringify(options.request)
47+
},
48+
Manifest: {
49+
DataType: "String",
50+
StringValue: JSON.stringify(options.manifest)
51+
},
52+
...(options.basePath
53+
? {
54+
BasePath: {
55+
DataType: "String",
56+
StringValue: options.basePath
57+
}
58+
}
59+
: {})
60+
},
61+
// We only want to trigger the regeneration once for every previous
62+
// update. This will prevent the case where this page is being
63+
// requested again whilst its already started to regenerate.
64+
MessageDeduplicationId: lastModifiedAt,
65+
// Only deduplicate based on the object, i.e. we can generate
66+
// different pages in parallel, just not the same one
67+
MessageGroupId: options.request.uri
68+
})
69+
);
70+
};

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export const handler: AWSLambda.SQSHandler = async (event) => {
5959
"passthrough"
6060
);
6161

62-
const expires = new Date(Date.now() + renderOpts.revalidate * 1000);
62+
const revalidate =
63+
renderOpts.revalidate ?? ssgRoute.initialRevalidateSeconds;
64+
const expires = new Date(Date.now() + revalidate * 1000);
6365
const s3BasePath = basePath ? `${basePath.replace(/^\//, "")}/` : "";
6466
const s3JsonParams = {
6567
Bucket: bucketName,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const s3BucketNameFromEventRequest = (
2+
request: AWSLambda.CloudFrontRequest
3+
): string | undefined => {
4+
const { region, domainName } = request.origin?.s3 || {};
5+
return domainName?.replace(`.s3.${region}.amazonaws.com`, "");
6+
};

packages/libs/lambda-at-edge/yarn.lock

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,9 +1501,9 @@ fast-base64-decode@^1.0.0:
15011501
integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==
15021502

15031503
fast-xml-parser@^3.16.0:
1504-
version "3.17.4"
1505-
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.17.4.tgz#d668495fb3e4bbcf7970f3c24ac0019d82e76477"
1506-
integrity sha512-qudnQuyYBgnvzf5Lj/yxMcf4L9NcVWihXJg7CiU1L+oUCq8MUnFEfH2/nXR/W5uq+yvUN1h7z6s7vs2v1WkL1A==
1504+
version "3.19.0"
1505+
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01"
1506+
integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==
15071507

15081508
fetch-mock-jest@^1.5.1:
15091509
version "1.5.1"
@@ -2399,9 +2399,9 @@ rc@^1.2.7:
23992399
strip-json-comments "~2.0.1"
24002400

24012401
react-native-get-random-values@^1.4.0:
2402-
version "1.5.0"
2403-
resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.5.0.tgz#91cda18f0e66e3d9d7660ba80c61c914030c1e05"
2404-
integrity sha512-LK+Wb8dEimJkd/dub7qziDmr9Tw4chhpzVeQ6JDo4czgfG4VXbptRyOMdu8503RiMF6y9pTH6ZUTkrrpprqT7w==
2402+
version "1.7.0"
2403+
resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.7.0.tgz#86d9d1960828b606392dba4540bf760605448530"
2404+
integrity sha512-zDhmpWUekGRFb9I+MQkxllHcqXN9HBSsgPwBQfrZ1KZYpzDspWLZ6/yLMMZrtq4pVqNR7C7N96L3SuLpXv1nhQ==
24052405
dependencies:
24062406
fast-base64-decode "^1.0.0"
24072407

yarn.lock

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,46 @@
178178
fast-xml-parser "^3.16.0"
179179
tslib "^2.0.0"
180180

181+
"@aws-sdk/[email protected]":
182+
version "1.0.0-rc.3"
183+
resolved "https://registry.yarnpkg.com/@aws-sdk/client-sqs/-/client-sqs-1.0.0-rc.3.tgz#aca468b52f77db00ffdf27d825022124d802da4d"
184+
integrity sha512-qEXJ++GJ46sPboyhRUJIv03buEvmXT5lLgjUdWjZKwzHaU34GPH0B7xxlLOUWmA+JvyPaK91ESjGqLc/82GLaA==
185+
dependencies:
186+
"@aws-crypto/sha256-browser" "^1.0.0"
187+
"@aws-crypto/sha256-js" "^1.0.0"
188+
"@aws-sdk/config-resolver" "1.0.0-rc.3"
189+
"@aws-sdk/credential-provider-node" "1.0.0-rc.3"
190+
"@aws-sdk/fetch-http-handler" "1.0.0-rc.3"
191+
"@aws-sdk/hash-node" "1.0.0-rc.3"
192+
"@aws-sdk/invalid-dependency" "1.0.0-rc.3"
193+
"@aws-sdk/md5-js" "1.0.0-rc.3"
194+
"@aws-sdk/middleware-content-length" "1.0.0-rc.3"
195+
"@aws-sdk/middleware-host-header" "1.0.0-rc.3"
196+
"@aws-sdk/middleware-logger" "1.0.0-rc.3"
197+
"@aws-sdk/middleware-retry" "1.0.0-rc.3"
198+
"@aws-sdk/middleware-sdk-sqs" "1.0.0-rc.3"
199+
"@aws-sdk/middleware-serde" "1.0.0-rc.3"
200+
"@aws-sdk/middleware-signing" "1.0.0-rc.3"
201+
"@aws-sdk/middleware-stack" "1.0.0-rc.3"
202+
"@aws-sdk/middleware-user-agent" "1.0.0-rc.3"
203+
"@aws-sdk/node-config-provider" "1.0.0-rc.3"
204+
"@aws-sdk/node-http-handler" "1.0.0-rc.3"
205+
"@aws-sdk/protocol-http" "1.0.0-rc.3"
206+
"@aws-sdk/smithy-client" "1.0.0-rc.3"
207+
"@aws-sdk/types" "1.0.0-rc.3"
208+
"@aws-sdk/url-parser-browser" "1.0.0-rc.3"
209+
"@aws-sdk/url-parser-node" "1.0.0-rc.3"
210+
"@aws-sdk/util-base64-browser" "1.0.0-rc.3"
211+
"@aws-sdk/util-base64-node" "1.0.0-rc.3"
212+
"@aws-sdk/util-body-length-browser" "1.0.0-rc.3"
213+
"@aws-sdk/util-body-length-node" "1.0.0-rc.3"
214+
"@aws-sdk/util-user-agent-browser" "1.0.0-rc.3"
215+
"@aws-sdk/util-user-agent-node" "1.0.0-rc.3"
216+
"@aws-sdk/util-utf8-browser" "1.0.0-rc.3"
217+
"@aws-sdk/util-utf8-node" "1.0.0-rc.3"
218+
fast-xml-parser "^3.16.0"
219+
tslib "^2.0.0"
220+
181221
"@aws-sdk/[email protected]":
182222
version "1.0.0-rc.3"
183223
resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-1.0.0-rc.3.tgz#0eb877cdabffb75ba3ed89f14e86301faeec12d2"
@@ -441,6 +481,15 @@
441481
"@aws-sdk/util-arn-parser" "1.0.0-rc.3"
442482
tslib "^1.8.0"
443483

484+
"@aws-sdk/[email protected]":
485+
version "1.0.0-rc.3"
486+
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-1.0.0-rc.3.tgz#5f02a97b0f34a4848ef8769e1e21d09d178d3cd8"
487+
integrity sha512-d3kL0IDQtXf/kP3RXMH6+AsjYS69tPC+9r9O28ri/qPDQFUdeHVFxybneAA/5JWikDM6tZ4htgkm+Tm4PUm5hA==
488+
dependencies:
489+
"@aws-sdk/types" "1.0.0-rc.3"
490+
"@aws-sdk/util-hex-encoding" "1.0.0-rc.3"
491+
tslib "^1.8.0"
492+
444493
"@aws-sdk/[email protected]":
445494
version "1.0.0-rc.3"
446495
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-1.0.0-rc.3.tgz#81307310c51d50ec8425bee9fb08d35a7458dcfc"

0 commit comments

Comments
 (0)