Skip to content

Commit

Permalink
revise view_request override api to improve cache hit ratio + geo hea…
Browse files Browse the repository at this point in the history
…ders
  • Loading branch information
coryasilva committed Dec 17, 2024
1 parent 79f78a5 commit 2ef35d8
Showing 1 changed file with 112 additions and 28 deletions.
140 changes: 112 additions & 28 deletions src/NextjsDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ export interface NextjsDistributionOverrides {
readonly staticBehaviorOptions?: AddBehaviorOptions;
readonly staticResponseHeadersPolicyProps?: cloudfront.ResponseHeadersPolicyProps;
readonly s3OriginProps?: OptionalS3OriginProps;
/**
* Cloudfront function code that runs on VIEWER_REQUEST.
* The following comments will be replaced with code snippets
* so you can customize this function.
*
* INJECT_CLOUDFRONT_FUNCTION_HOST_HEADER: Add the required x-forwarded-host header.
* INJECT_CLOUDFRONT_FUNCTION_CACHE_HEADER_KEY: Improves open-next cache key.
* INJECT_CLOUDFRONT_FUNCTION_GEO_HEADERS: Enables open-next geo headers.
*
* @default
* async function handler(event) {
* // INJECT_CLOUDFRONT_FUNCTION_HOST_HEADER
* // INJECT_CLOUDFRONT_FUNCTION_CACHE_HEADER_KEY
* // INJECT_CLOUDFRONT_FUNCTION_GEO_HEADERS
* }
*/
readonly viewerRequestFunctionCode?: string;
}

export interface NextjsDistributionProps {
Expand Down Expand Up @@ -272,14 +289,7 @@ export class NextjsDistribution extends Construct {
serverBehaviorOptions?.cachePolicy ??
new cloudfront.CachePolicy(this, 'ServerCachePolicy', {
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
headerBehavior: cloudfront.CacheHeaderBehavior.allowList(
'accept',
'rsc',
'next-router-prefetch',
'next-router-state-tree',
'next-url',
'x-prerender-revalidate'
),
headerBehavior: cloudfront.CacheHeaderBehavior.allowList('x-open-next-cache-key'),
cookieBehavior: cloudfront.CacheCookieBehavior.all(),
defaultTtl: Duration.seconds(0),
maxTtl: Duration.days(365),
Expand All @@ -301,7 +311,7 @@ export class NextjsDistribution extends Construct {
override: false,
// MDN Cache-Control Use Case: Up-to-date contents always
// @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#up-to-date_contents_always
value: `no-cache`,
value: 'no-cache',
},
],
},
Expand All @@ -316,28 +326,107 @@ export class NextjsDistribution extends Construct {
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
edgeLambdas: this.edgeLambdas.length ? this.edgeLambdas : undefined,
functionAssociations:
this.props.overrides?.distributionProps?.defaultBehavior?.functionAssociations ??
this.createCloudFrontFnAssociations(),
functionAssociations: this.createCloudFrontFnAssociations(),
cachePolicy,
responseHeadersPolicy,
...serverBehaviorOptions,
};
}

private useCloudFrontFunctionHostHeader() {
return `event.request.headers["x-forwarded-host"] = event.request.headers.host;`;
}

private useCloudFrontFunctionCacheHeaderKey() {
// This function is used to improve cache hit ratio by setting the cache key
// based on the request headers and the path. `next/image` only needs the
// accept header, and this header is not useful for the rest of the query
return `
const getHeader = (key) => {
const header = event.request.headers[key];
if (header) {
if (header.multiValue) {
return header.multiValue.map((header) => header.value).join(",");
}
if (header.value) {
return header.value;
}
}
return "";
}
let cacheKey = "";
if (event.request.uri.startsWith("/_next/image")) {
cacheKey = getHeader("accept");
} else {
cacheKey =
getHeader("rsc") +
getHeader("next-router-prefetch") +
getHeader("next-router-state-tree") +
getHeader("next-url") +
getHeader("x-prerender-revalidate");
}
if (event.request.cookies["__prerender_bypass"]) {
cacheKey += event.request.cookies["__prerender_bypass"]
? event.request.cookies["__prerender_bypass"].value
: "";
}
const crypto = require("crypto")
const hashedKey = crypto.createHash("md5").update(cacheKey).digest("hex");
event.request.headers["x-open-next-cache-key"] = { value: hashedKey };
`;
}

private useCloudfrontFunctionGeoHeaders() {
return `
if(event.request.headers["cloudfront-viewer-city"]) {
event.request.headers["x-open-next-city"] = event.request.headers["cloudfront-viewer-city"];
}
if(event.request.headers["cloudfront-viewer-country"]) {
event.request.headers["x-open-next-country"] = event.request.headers["cloudfront-viewer-country"];
}
if(event.request.headers["cloudfront-viewer-region"]) {
event.request.headers["x-open-next-region"] = event.request.headers["cloudfront-viewer-region"];
}
if(event.request.headers["cloudfront-viewer-latitude"]) {
event.request.headers["x-open-next-latitude"] = event.request.headers["cloudfront-viewer-latitude"];
}
if(event.request.headers["cloudfront-viewer-longitude"]) {
event.request.headers["x-open-next-longitude"] = event.request.headers["cloudfront-viewer-longitude"];
}
`;
}

/**
* If this doesn't run, then Next.js Server's `request.url` will be Lambda Function
* URL instead of domain
*/
private createCloudFrontFnAssociations() {
let code =
this.props.overrides?.viewerRequestFunctionCode ??
`
async function handler(event) {
// INJECT_CLOUDFRONT_FUNCTION_HOST_HEADER
// INJECT_CLOUDFRONT_FUNCTION_CACHE_HEADER_KEY
// INJECT_CLOUDFRONT_FUNCTION_GEO_HEADERS
}
`;
code = code.replace(
/^\s*\/\/\s*INJECT_CLOUDFRONT_FUNCTION_HOST_HEADER.*$/i,
this.useCloudFrontFunctionHostHeader()
);
code = code.replace(
/^\s*\/\/\s*INJECT_CLOUDFRONT_FUNCTION_CACHE_HEADER_KEY.*$/i,
this.useCloudFrontFunctionCacheHeaderKey()
);
code = code.replace(
/^\s*\/\/\s*INJECT_CLOUDFRONT_FUNCTION_GEO_HEADERS.*$/i,
this.useCloudfrontFunctionGeoHeaders()
);
const cloudFrontFn = new cloudfront.Function(this, 'CloudFrontFn', {
code: cloudfront.FunctionCode.fromInline(`
function handler(event) {
var request = event.request;
request.headers["x-forwarded-host"] = request.headers.host;
return request;
}
`),
code: cloudfront.FunctionCode.fromInline(code),
});
return [{ eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, function: cloudFrontFn }];
}
Expand Down Expand Up @@ -378,7 +467,7 @@ export class NextjsDistribution extends Construct {
override: false,
// MDN Cache-Control Use Case: Up-to-date contents always
// @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#up-to-date_contents_always
value: `no-cache`,
value: 'no-cache',
},
],
},
Expand Down Expand Up @@ -436,20 +525,15 @@ export class NextjsDistribution extends Construct {
* create a CloudFront Distribution if one is passed in by user.
*/
private createCloudFrontDistribution() {
const distributionProps = this.props.overrides?.distributionProps;
const { defaultBehavior: defaultBehaviorOverrides = {}, ...props } = distributionProps ?? {};
return new cloudfront.Distribution(this, 'Distribution', {
// defaultRootObject: "index.html",
defaultRootObject: '',
minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
domainNames: this.props.nextDomain?.domainNames,
certificate: this.props.nextDomain?.certificate,
// these values can NOT be overwritten by cfDistributionProps
defaultBehavior: {
...this.serverBehaviorOptions,
...defaultBehaviorOverrides,
},
...props,
defaultBehavior: this.serverBehaviorOptions,
...this.props.overrides?.distributionProps,
});
}

Expand Down Expand Up @@ -481,7 +565,7 @@ export class NextjsDistribution extends Construct {
});
if (publicFiles.length >= 25) {
throw new Error(
`Too many public/ files in Next.js build. CloudFront limits Distributions to 25 Cache Behaviors. See documented limit here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-web-distributions`
'Too many public/ files in Next.js build. CloudFront limits Distributions to 25 Cache Behaviors. See documented limit here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-web-distributions'
);
}
for (const publicFile of publicFiles) {
Expand Down

0 comments on commit 2ef35d8

Please sign in to comment.