Skip to content

Commit 1721ce6

Browse files
feat: added cache fragment header to response
1 parent f6a8943 commit 1721ce6

File tree

5 files changed

+88
-1
lines changed

5 files changed

+88
-1
lines changed

src/cacheHandler/strategy/s3.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export class S3Cache implements CacheStrategy {
6666
const baseInput: PutObjectCommandInput = {
6767
Bucket: this.bucketName,
6868
Key: `${pageKey}/${cacheKey}`,
69+
Metadata: {
70+
'Cache-Fragment-Key': cacheKey
71+
},
6972
...(data.revalidate ? { CacheControl: `max-age=${data.revalidate}` } : undefined)
7073
}
7174

src/cdk/constructs/CloudFrontDistribution.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface CloudFrontPropsDistribution {
1212
renderServerDomain: string
1313
requestEdgeFunction: cloudfront.experimental.EdgeFunction
1414
responseEdgeFunction: cloudfront.experimental.EdgeFunction
15+
viewerResponseEdgeFunction: cloudfront.experimental.EdgeFunction
1516
cacheConfig: CacheConfig
1617
imageTTL?: number
1718
}
@@ -29,7 +30,15 @@ export class CloudFrontDistribution extends Construct {
2930
constructor(scope: Construct, id: string, props: CloudFrontPropsDistribution) {
3031
super(scope, id)
3132

32-
const { staticBucket, requestEdgeFunction, responseEdgeFunction, cacheConfig, renderServerDomain, imageTTL } = props
33+
const {
34+
staticBucket,
35+
requestEdgeFunction,
36+
responseEdgeFunction,
37+
viewerResponseEdgeFunction,
38+
cacheConfig,
39+
renderServerDomain,
40+
imageTTL
41+
} = props
3342

3443
const splitCachePolicy = new cloudfront.CachePolicy(this, 'SplitCachePolicy', {
3544
cachePolicyName: `${id}-SplitCachePolicy`,
@@ -86,6 +95,10 @@ export class CloudFrontDistribution extends Construct {
8695
{
8796
functionVersion: responseEdgeFunction.currentVersion,
8897
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE
98+
},
99+
{
100+
functionVersion: viewerResponseEdgeFunction.currentVersion,
101+
eventType: cloudfront.LambdaEdgeEventType.VIEWER_RESPONSE
89102
}
90103
],
91104
cachePolicy: splitCachePolicy
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Construct } from 'constructs'
2+
import * as lambda from 'aws-cdk-lib/aws-lambda'
3+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
4+
import path from 'node:path'
5+
import { buildLambda } from '../../build/edge'
6+
7+
const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
8+
'18': lambda.Runtime.NODEJS_18_X,
9+
'20': lambda.Runtime.NODEJS_20_X
10+
}
11+
12+
interface ViewerResponseLambdaEdgeProps {
13+
nodejs?: string
14+
buildOutputPath: string
15+
}
16+
17+
export class ViewerResponseLambdaEdge extends Construct {
18+
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction
19+
20+
constructor(scope: Construct, id: string, props: ViewerResponseLambdaEdgeProps) {
21+
const { nodejs, buildOutputPath } = props
22+
super(scope, id)
23+
24+
const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20']
25+
const name = 'viewerResponse'
26+
27+
buildLambda(name, buildOutputPath)
28+
29+
this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'ViewerResponseLambdaEdge', {
30+
runtime: nodeJSEnvironment,
31+
code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', name)),
32+
handler: 'index.handler'
33+
})
34+
}
35+
}

src/cdk/stacks/NextCloudfrontStack.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { RoutingLambdaEdge } from '../constructs/RoutingLambdaEdge'
55
import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution'
66
import { CacheConfig } from '../../types'
77
import { CheckExpirationLambdaEdge } from '../constructs/CheckExpirationLambdaEdge'
8+
import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge'
89

910
export interface NextCloudfrontStackProps extends StackProps {
1011
nodejs?: string
@@ -21,6 +22,7 @@ export interface NextCloudfrontStackProps extends StackProps {
2122
export class NextCloudfrontStack extends Stack {
2223
public readonly routingLambdaEdge: RoutingLambdaEdge
2324
public readonly checkExpLambdaEdge: CheckExpirationLambdaEdge
25+
public readonly viewerResponseLambdaEdge: ViewerResponseLambdaEdge
2426
public readonly cloudfront: CloudFrontDistribution
2527

2628
constructor(scope: Construct, id: string, props: NextCloudfrontStackProps) {
@@ -55,6 +57,11 @@ export class NextCloudfrontStack extends Stack {
5557
region
5658
})
5759

60+
this.viewerResponseLambdaEdge = new ViewerResponseLambdaEdge(this, `${id}-ViewerResponseLambdaEdge`, {
61+
nodejs,
62+
buildOutputPath
63+
})
64+
5865
const staticBucket = s3.Bucket.fromBucketAttributes(this, `${id}-StaticAssetsBucket`, {
5966
bucketName: staticBucketName,
6067
region
@@ -65,6 +72,7 @@ export class NextCloudfrontStack extends Stack {
6572
renderServerDomain,
6673
requestEdgeFunction: this.routingLambdaEdge.lambdaEdge,
6774
responseEdgeFunction: this.checkExpLambdaEdge.lambdaEdge,
75+
viewerResponseEdgeFunction: this.viewerResponseLambdaEdge.lambdaEdge,
6876
cacheConfig,
6977
imageTTL
7078
})

src/lambdas/viewerResponse.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { CloudFrontRequestCallback, Context, CloudFrontResponseEvent } from 'aws-lambda'
2+
3+
/**
4+
* Lambda@Edge viewer response handler that processes CloudFront responses.
5+
* This handler extracts the cache fragment key from x-amz-meta headers and
6+
* sets it as a standard Cache-Fragment-Key header while removing the original
7+
* x-amz-meta header.
8+
*
9+
* @param {CloudFrontResponseEvent} event - The CloudFront response event object
10+
* @param {Context} _context - AWS Lambda context object (unused)
11+
* @param {CloudFrontRequestCallback} callback - Callback to return the modified response
12+
* @returns {Promise<void>} - Returns nothing, uses callback to return response
13+
*/
14+
export const handler = async (
15+
event: CloudFrontResponseEvent,
16+
_context: Context,
17+
callback: CloudFrontRequestCallback
18+
) => {
19+
const response = event.Records[0].cf.response
20+
const fileCacheKey = response.headers['x-amz-meta-cache-fragment-key']?.[0].value
21+
22+
if (fileCacheKey) {
23+
response.headers['cache-fragment-key'] = [{ key: 'Cache-Fragment-Key', value: fileCacheKey }]
24+
response.headers['x-amz-meta-cache-fragment-key'] = []
25+
}
26+
27+
callback(null, response)
28+
}

0 commit comments

Comments
 (0)