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

Commit

Permalink
use expires header rather than last-modified and a small code tidy
Browse files Browse the repository at this point in the history
  • Loading branch information
kirkness committed Apr 29, 2021
1 parent 6f1ce93 commit fc8bd38
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 82 deletions.
94 changes: 22 additions & 72 deletions packages/libs/lambda-at-edge/src/default-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ import {
getLocalePrefixFromUri
} from "./routing/locale-utils";
import { removeBlacklistedHeaders } from "./headers/removeBlacklistedHeaders";
import { getStaticRegenerationResponse } from "./lib/getStaticRegenerationResponse";
import { s3BucketNameFromEventRequest } from "./s3/s3BucketNameFromEventRequest";
import { triggerStaticRegeneration } from "./lib/triggerStaticRegeneration";

const basePath = RoutesManifestJson.basePath;

Expand Down Expand Up @@ -590,7 +593,6 @@ const handleOriginRequest = async ({
const handleOriginResponse = async ({
event,
manifest,
prerenderManifest,
routesManifest
}: {
event: OriginResponseEvent;
Expand All @@ -602,8 +604,7 @@ const handleOriginResponse = async ({
const request = event.Records[0].cf.request;
const { uri } = request;
const { status } = response;
const { region, domainName } = request.origin?.s3 || {};
const bucketName = domainName?.replace(`.s3.${region}.amazonaws.com`, "");
const bucketName = s3BucketNameFromEventRequest(request);

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

const initialRevalidateSeconds =
manifest.pages.ssg.nonDynamic?.[uri.replace(".html", "")]
?.initialRevalidateSeconds;
const lastModifiedHeaderString =
response.headers?.["last-modified"]?.[0]?.value;
const lastModifiedAt = lastModifiedHeaderString
? new Date(lastModifiedHeaderString)
: null;
if (typeof initialRevalidateSeconds === "number" && lastModifiedAt) {
/**
* TODO: Refactor to use the returned `Expired` header.
*/
const createdAgo =
(Date.now() - (lastModifiedAt.getTime() || Date.now())) / 1000;

const timeToRevalidate = Math.floor(
initialRevalidateSeconds - createdAgo
);
const staticRegenerationResponse = getStaticRegenerationResponse({
requestedOriginUri: uri,
expiresHeader: response.headers.expires?.[0]?.value || "",
manifest
});

if (staticRegenerationResponse) {
response.headers["cache-control"] = [
{
key: "Cache-Control",
value:
timeToRevalidate < 0
? "public, max-age=0, s-maxage=0, must-revalidate"
: `public, max-age=0, s-maxage=${timeToRevalidate}, must-revalidate`
value: staticRegenerationResponse.cacheControl
}
];

if (timeToRevalidate < 0) {
const { SQSClient, SendMessageCommand } = await import(
"@aws-sdk/client-sqs"
);
const sqs = new SQSClient({
region,
maxAttempts: 3,
retryStrategy: await buildS3RetryStrategy()
// We don't want the `expires` header to be sent to the client we manage
// the cache at the edge using the s-maxage directive in the cache-control
// header
delete response.headers.expires;

if (staticRegenerationResponse.secondsRemainingUntilRevalidation === 0) {
await triggerStaticRegeneration({
basePath,
manifest,
request,
response
});
await sqs.send(
new SendMessageCommand({
QueueUrl: `https://sqs.${region}.amazonaws.com/${bucketName}.fifo`,
MessageBody: uri,
MessageAttributes: {
BucketRegion: {
DataType: "String",
StringValue: region
},
BucketName: {
DataType: "String",
StringValue: bucketName
},
CloudFrontEventRequest: {
DataType: "String",
StringValue: JSON.stringify(request)
},
Manifest: {
DataType: "String",
StringValue: JSON.stringify(manifest)
},
...(basePath
? {
BasePath: {
DataType: "String",
StringValue: basePath
}
}
: {})
},
// We only want to trigger the regeneration once for every previous
// update. This will prevent the case where this page is being
// requested again whilst its already started to regenerate.
MessageDeduplicationId: lastModifiedAt.getTime().toString(),
// Only deduplicate based on the object, i.e. we can generate
// different pages in parallel, just not the same one
MessageGroupId: uri
})
);
}
}

Expand Down
7 changes: 4 additions & 3 deletions packages/libs/lambda-at-edge/src/image-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "./routing/redirector";
import { getUnauthenticatedResponse } from "./auth/authenticator";
import { removeBlacklistedHeaders } from "./headers/removeBlacklistedHeaders";
import { s3BucketNameFromEventRequest } from "./s3/s3BucketNameFromEventRequest";

const basePath = RoutesManifestJson.basePath;

Expand Down Expand Up @@ -88,11 +89,11 @@ export const handler = async (
true
);

const { domainName, region } = request.origin!.s3!;
const bucketName = domainName.replace(`.s3.${region}.amazonaws.com`, "");
const { region } = request.origin!.s3!;
const bucketName = s3BucketNameFromEventRequest(request);

await imageOptimizer(
{ basePath: basePath, bucketName: bucketName, region: region },
{ basePath: basePath, bucketName: bucketName || "", region: region },
imagesManifest,
req,
res,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { OriginRequestDefaultHandlerManifest } from "../types";

interface StaticRegenerationResponseOptions {
// URI of the origin object
requestedOriginUri: string;
// Header as set on the origin object
expiresHeader: string;
manifest: OriginRequestDefaultHandlerManifest;
}

interface StaticRegenerationResponseValue {
// Cache-Control header
cacheControl: string;
secondsRemainingUntilRevalidation: number;
}

/**
* Function called within an origin response as part of the Incremental Static
* Regeneration logic. Returns required headers for the response, or false if
* this response is not compatible with ISR.
*/
const getStaticRegenerationResponse = (
options: StaticRegenerationResponseOptions
): StaticRegenerationResponseValue | false => {
const initialRevalidateSeconds =
options.manifest.pages.ssg.nonDynamic?.[
options.requestedOriginUri.replace(".html", "")
]?.initialRevalidateSeconds;

// If this page did not write a revalidate value at build time it is not an
// ISR page

This comment has been minimized.

Copy link
@jvarho

jvarho Apr 29, 2021

Collaborator

Like I wrote in elsewhere, dynamic pages do not have initialRevalidateSeconds in the manifest. The only way to know that they should be revalidated is to store that information in S3 – i.e. the Expires header you now use.

This comment has been minimized.

Copy link
@kirkness

kirkness Apr 29, 2021

Author Collaborator

👌

if (typeof initialRevalidateSeconds !== "number") {
return false;
}

const expiresAt = new Date(options.expiresHeader);

// isNaN will resolve true on initial load of this page (as the expiresHeader
// won't be set), in which case we trigger a regeneration now
const secondsRemainingUntilRevalidation = isNaN(expiresAt.getTime())
? 0
: // Never return a negative amount of seconds if revalidation could have
// happened sooner
Math.floor(Math.max(0, (expiresAt.getTime() - Date.now()) / 1000));

return {
secondsRemainingUntilRevalidation,
cacheControl: `public, max-age=0, s-maxage=${secondsRemainingUntilRevalidation}, must-revalidate`
};
};

export { getStaticRegenerationResponse };
70 changes: 70 additions & 0 deletions packages/libs/lambda-at-edge/src/lib/triggerStaticRegeneration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { s3BucketNameFromEventRequest } from "../s3/s3BucketNameFromEventRequest";
import { buildS3RetryStrategy } from "../s3/s3RetryStrategy";
import { OriginRequestDefaultHandlerManifest } from "../types";

interface TriggerStaticRegenerationOptions {
request: AWSLambda.CloudFrontRequest;
response: AWSLambda.CloudFrontResponse;
manifest: OriginRequestDefaultHandlerManifest;
basePath: string | undefined;
}

export const triggerStaticRegeneration = async (
options: TriggerStaticRegenerationOptions
): Promise<void> => {
const { region } = options.request.origin?.s3 || {};
const bucketName = s3BucketNameFromEventRequest(options.request);

const { SQSClient, SendMessageCommand } = await import("@aws-sdk/client-sqs");
const sqs = new SQSClient({
region,
maxAttempts: 3,
retryStrategy: await buildS3RetryStrategy()
});

const lastModifiedAt = new Date(
options.response.headers["last-modified"]?.[0].value
)
.getTime()
.toString();

await sqs.send(
new SendMessageCommand({
QueueUrl: `https://sqs.${region}.amazonaws.com/${bucketName}.fifo`,
MessageBody: options.request.uri, // This is not used, however it is a required property
MessageAttributes: {
BucketRegion: {
DataType: "String",
StringValue: region
},
BucketName: {
DataType: "String",
StringValue: bucketName
},
CloudFrontEventRequest: {
DataType: "String",
StringValue: JSON.stringify(options.request)
},
Manifest: {
DataType: "String",
StringValue: JSON.stringify(options.manifest)
},
...(options.basePath
? {
BasePath: {
DataType: "String",
StringValue: options.basePath
}
}
: {})
},
// We only want to trigger the regeneration once for every previous
// update. This will prevent the case where this page is being
// requested again whilst its already started to regenerate.
MessageDeduplicationId: lastModifiedAt,
// Only deduplicate based on the object, i.e. we can generate
// different pages in parallel, just not the same one
MessageGroupId: options.request.uri
})
);
};
4 changes: 3 additions & 1 deletion packages/libs/lambda-at-edge/src/regeneration-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export const handler: AWSLambda.SQSHandler = async (event) => {
"passthrough"
);

const expires = new Date(Date.now() + renderOpts.revalidate * 1000);
const revalidate =
renderOpts.revalidate ?? ssgRoute.initialRevalidateSeconds;

This comment has been minimized.

Copy link
@jvarho

jvarho Apr 29, 2021

Collaborator

Next.js server allows a regenerated page to return no revalidate value, in which case it becomes static (and should have the same cache-control as normal SSG pages).

Very unlikely corner-case in practice, I know...

const expires = new Date(Date.now() + revalidate * 1000);
const s3BasePath = basePath ? `${basePath.replace(/^\//, "")}/` : "";
const s3JsonParams = {
Bucket: bucketName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const s3BucketNameFromEventRequest = (
request: AWSLambda.CloudFrontRequest
): string | undefined => {
const { region, domainName } = request.origin?.s3 || {};
return domainName?.replace(`.s3.${region}.amazonaws.com`, "");
};
12 changes: 6 additions & 6 deletions packages/libs/lambda-at-edge/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1501,9 +1501,9 @@ fast-base64-decode@^1.0.0:
integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==

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

fetch-mock-jest@^1.5.1:
version "1.5.1"
Expand Down Expand Up @@ -2399,9 +2399,9 @@ rc@^1.2.7:
strip-json-comments "~2.0.1"

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

Expand Down
49 changes: 49 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,46 @@
fast-xml-parser "^3.16.0"
tslib "^2.0.0"

"@aws-sdk/[email protected]":
version "1.0.0-rc.3"
resolved "https://registry.yarnpkg.com/@aws-sdk/client-sqs/-/client-sqs-1.0.0-rc.3.tgz#aca468b52f77db00ffdf27d825022124d802da4d"
integrity sha512-qEXJ++GJ46sPboyhRUJIv03buEvmXT5lLgjUdWjZKwzHaU34GPH0B7xxlLOUWmA+JvyPaK91ESjGqLc/82GLaA==
dependencies:
"@aws-crypto/sha256-browser" "^1.0.0"
"@aws-crypto/sha256-js" "^1.0.0"
"@aws-sdk/config-resolver" "1.0.0-rc.3"
"@aws-sdk/credential-provider-node" "1.0.0-rc.3"
"@aws-sdk/fetch-http-handler" "1.0.0-rc.3"
"@aws-sdk/hash-node" "1.0.0-rc.3"
"@aws-sdk/invalid-dependency" "1.0.0-rc.3"
"@aws-sdk/md5-js" "1.0.0-rc.3"
"@aws-sdk/middleware-content-length" "1.0.0-rc.3"
"@aws-sdk/middleware-host-header" "1.0.0-rc.3"
"@aws-sdk/middleware-logger" "1.0.0-rc.3"
"@aws-sdk/middleware-retry" "1.0.0-rc.3"
"@aws-sdk/middleware-sdk-sqs" "1.0.0-rc.3"
"@aws-sdk/middleware-serde" "1.0.0-rc.3"
"@aws-sdk/middleware-signing" "1.0.0-rc.3"
"@aws-sdk/middleware-stack" "1.0.0-rc.3"
"@aws-sdk/middleware-user-agent" "1.0.0-rc.3"
"@aws-sdk/node-config-provider" "1.0.0-rc.3"
"@aws-sdk/node-http-handler" "1.0.0-rc.3"
"@aws-sdk/protocol-http" "1.0.0-rc.3"
"@aws-sdk/smithy-client" "1.0.0-rc.3"
"@aws-sdk/types" "1.0.0-rc.3"
"@aws-sdk/url-parser-browser" "1.0.0-rc.3"
"@aws-sdk/url-parser-node" "1.0.0-rc.3"
"@aws-sdk/util-base64-browser" "1.0.0-rc.3"
"@aws-sdk/util-base64-node" "1.0.0-rc.3"
"@aws-sdk/util-body-length-browser" "1.0.0-rc.3"
"@aws-sdk/util-body-length-node" "1.0.0-rc.3"
"@aws-sdk/util-user-agent-browser" "1.0.0-rc.3"
"@aws-sdk/util-user-agent-node" "1.0.0-rc.3"
"@aws-sdk/util-utf8-browser" "1.0.0-rc.3"
"@aws-sdk/util-utf8-node" "1.0.0-rc.3"
fast-xml-parser "^3.16.0"
tslib "^2.0.0"

"@aws-sdk/[email protected]":
version "1.0.0-rc.3"
resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-1.0.0-rc.3.tgz#0eb877cdabffb75ba3ed89f14e86301faeec12d2"
Expand Down Expand Up @@ -441,6 +481,15 @@
"@aws-sdk/util-arn-parser" "1.0.0-rc.3"
tslib "^1.8.0"

"@aws-sdk/[email protected]":
version "1.0.0-rc.3"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-1.0.0-rc.3.tgz#5f02a97b0f34a4848ef8769e1e21d09d178d3cd8"
integrity sha512-d3kL0IDQtXf/kP3RXMH6+AsjYS69tPC+9r9O28ri/qPDQFUdeHVFxybneAA/5JWikDM6tZ4htgkm+Tm4PUm5hA==
dependencies:
"@aws-sdk/types" "1.0.0-rc.3"
"@aws-sdk/util-hex-encoding" "1.0.0-rc.3"
tslib "^1.8.0"

"@aws-sdk/[email protected]":
version "1.0.0-rc.3"
resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-1.0.0-rc.3.tgz#81307310c51d50ec8425bee9fb08d35a7458dcfc"
Expand Down

0 comments on commit fc8bd38

Please sign in to comment.