From e88ff0557f845e6cd57a9c0bf12e5898964f08ea Mon Sep 17 00:00:00 2001 From: Khue Nguyen Date: Mon, 5 Dec 2022 11:10:49 -0800 Subject: [PATCH] chore: pr feedback; fix cache-control headers --- README.md | 2 +- assets/lambda/ImageOptimization/index.ts | 40 +++++++++++------------- assets/lambda/NextJsHandler.ts | 7 +++-- src/ImageOptimizationLambda.ts | 7 ++++- src/NextjsBuild.ts | 19 ----------- src/NextjsDistribution.ts | 1 - 6 files changed, 29 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 1bcc6178..d962332b 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ class NextjsSst extends Nextjs { ## Breaking changes -- v2.0.0: SST wrapper changed, lambda/assets/distribution defaults now are in the `defaults` prop, refactored distribution settings into the new NextjsDistribution construct. If you are upgrading, you must temporarily remove the `customDomain` on your existing 1.x.x app before upgrading to >=2.x.x because the CloudFront distribution will get recreated due to refactoring, and the custom domain must be globally unique across all CloudFront distibutions. Prepare for downtime. +- v2.0.0: SST wrapper changed, lambda/assets/distribution defaults now are in the `defaults` prop, refactored distribution settings into the new NextjsDistribution construct. If you are upgrading, you must temporarily remove the `customDomain` on your existing 1.x.x app before upgrading to >=2.x.x because the CloudFront distribution will get recreated due to refactoring, and the custom domain must be globally unique across all CloudFront distributions. Prepare for downtime. ## To-do diff --git a/assets/lambda/ImageOptimization/index.ts b/assets/lambda/ImageOptimization/index.ts index 1814e453..e3e47a0c 100644 --- a/assets/lambda/ImageOptimization/index.ts +++ b/assets/lambda/ImageOptimization/index.ts @@ -2,25 +2,21 @@ // There are other open source MIT libraries we can pick, but this seems the most straightforward import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3' -import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda' +import type { APIGatewayProxyHandlerV2 } from 'aws-lambda' import { IncomingMessage, ServerResponse } from 'http' import { defaultConfig, NextConfigComplete } from 'next/dist/server/config-shared' import { imageOptimizer as nextImageOptimizer, ImageOptimizerCache } from 'next/dist/server/image-optimizer' import { NextUrlWithParsedQuery } from 'next/dist/server/request-meta' import { ImageConfigComplete, ImageConfig } from 'next/dist/shared/lib/image-config' -import { NextConfig } from 'next' import { Writable } from 'node:stream' import https from 'node:https' -import path from 'node:path' -import fs from 'node:fs' +import { getNextServerConfig } from '../utils' const sourceBucket = process.env.S3_SOURCE_BUCKET ?? undefined // The next config file was bundled and outputted to the root // SEE: src/ImageOptimizationLambda.ts, Line 81 -const requiredServerFilesPath = path.join(__dirname, 'required-server-files.json'); -const json = fs.readFileSync(requiredServerFilesPath, 'utf-8'); -const { config } = JSON.parse(json) as { version: number; config: NextConfig }; +const { config } = getNextServerConfig() const pipeRes = (w: Writable, res: ServerResponse) => { w.pipe(res) @@ -46,14 +42,13 @@ const requestHandler = let response: any let data: Buffer // External url, try to fetch image - if ( !!url.href?.toLowerCase().startsWith('http')) { + if (url.href?.toLowerCase().startsWith('http')) { try { pipeRes(https.get(url), res) } catch (err) { console.error('Failed to get image', err) res.statusCode = 400 res.end() - return } } else { // S3 expects keys without leading `/` @@ -62,18 +57,11 @@ const requestHandler = const client = new S3Client({}) response = await client.send(new GetObjectCommand({ Bucket: bucketName, Key: trimmedKey })) if (!response.Body) { + res.setHeader('Cache-Control', 'no-store,no-cache,must-revalidate,proxy-revalidate') throw new Error(`Could not fetch image ${trimmedKey} from bucket.`) } - const stream = response.Body as Writable - pipeRes(stream, res) - - // Respect the bucket file's content-type and cace-control - if (response.ContentType) { - res.setHeader('Content-Type', response.ContentType) - } - if (response.CacheControl) { - res.setHeader('Cache-Control', response.CacheControl) - } + pipeRes(response.Body, res) + } } @@ -89,8 +77,6 @@ const nextConfig: NextConfigComplete = { }, } -// We don't need serverless-http neither basePath configuration as endpoint works as single route API. -// Images are handled via header and query param information. const optimizer: APIGatewayProxyHandlerV2 = async (event) => { try { if (!sourceBucket) { @@ -116,12 +102,22 @@ const optimizer: APIGatewayProxyHandlerV2 = async (event) => { statusCode: 200, body: optimizedResult.buffer.toString('base64'), isBase64Encoded: true, - headers: { Vary: 'Accept', 'Content-Type': optimizedResult.contentType }, + headers: { + Vary: 'Accept', + 'Cache-Control': `public,max-age=${optimizedResult.maxAge},immutable`, + 'Content-Type': optimizedResult.contentType + }, } } catch (error: any) { console.error(error) return { statusCode: 500, + headers: { + Vary: 'Accept', + // For failed images, allow client to retry after 1 hour. + 'Cache-Control': `public,max-age=3600,immutable`, + 'Content-Type': 'application/json' + }, body: error?.message || error?.toString() || error, } } diff --git a/assets/lambda/NextJsHandler.ts b/assets/lambda/NextJsHandler.ts index d0d2887a..13129f13 100644 --- a/assets/lambda/NextJsHandler.ts +++ b/assets/lambda/NextJsHandler.ts @@ -10,10 +10,10 @@ import fs from 'node:fs'; import { IncomingMessage, ServerResponse } from 'node:http'; import path from 'node:path'; import type { APIGatewayProxyHandlerV2 } from 'aws-lambda'; -import type { NextConfig } from 'next'; import type { Options } from 'next/dist/server/next-server'; import * as nss from 'next/dist/server/next-server'; import slsHttp from 'serverless-http'; +import { getNextServerConfig } from './utils' const getErrMessage = (e: any) => ({ message: 'Server failed to respond.', details: e }); @@ -28,7 +28,8 @@ const NextNodeServer: typeof nss.default = (nss.default as any)?.default ?? nss. const nextDir = path.join(__dirname, '.next'); const requiredServerFilesPath = path.join(nextDir, 'required-server-files.json'); const json = fs.readFileSync(requiredServerFilesPath, 'utf-8'); -const requiredServerFiles = JSON.parse(json) as { version: number; config: NextConfig }; +const { config: nextConfig } = getNextServerConfig() + const config: Options = { // hostname and port must be defined for proxying to work (middleware) hostname: 'localhost', @@ -36,7 +37,7 @@ const config: Options = { // Next.js compression should be disabled because of a bug // in the bundled `compression` package. See: // https://github.com/vercel/next.js/issues/11669 - conf: { ...requiredServerFiles.config, compress: false }, + conf: { ...nextConfig, compress: false }, customServer: false, dev: false, dir: __dirname, diff --git a/src/ImageOptimizationLambda.ts b/src/ImageOptimizationLambda.ts index d27a4979..50390796 100644 --- a/src/ImageOptimizationLambda.ts +++ b/src/ImageOptimizationLambda.ts @@ -78,7 +78,12 @@ export class ImageOptimizationLambda extends NodejsFunction { bundling: { commandHooks: { beforeBundling(inputDir: string, outputDir: string): string[] { - return [`echo '${data}' > ${inputDir}/${configFile}`, `cp ${inputDir}/${configFile} ${outputDir}`]; + // Saves the required-server-files.json to the .next folder + return [ + `echo '${data}' > ${inputDir}/${configFile}`, + `mkdir ${outputDir}/.next`, + `cp ${inputDir}/${configFile} ${outputDir}/.next`, + ]; }, afterBundling() { return []; diff --git a/src/NextjsBuild.ts b/src/NextjsBuild.ts index f95f0504..05f9c8e0 100644 --- a/src/NextjsBuild.ts +++ b/src/NextjsBuild.ts @@ -4,7 +4,6 @@ import { Token } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as spawn from 'cross-spawn'; import * as fs from 'fs-extra'; -import * as glob from 'glob'; import { listDirectory } from './NextjsAssetsDeployment'; import { CompressionLevel, NextjsBaseProps } from './NextjsBase'; @@ -126,24 +125,6 @@ export class NextjsBuild extends Construct { if (buildResult.status !== 0) { throw new Error('The app "build" script failed.'); } - - // cleanup - // delete the `sharp` module since it's provided by the lambda layer - const sharpPathNpm = path.join(this._getNextStandaloneDir(), 'node_modules', 'sharp'); // npm/yarn - if (fs.existsSync(sharpPathNpm)) { - // delete the sharp folder - if (!this.props.quiet) console.debug('├ Deleting sharp module from', sharpPathNpm); - fs.removeSync(sharpPathNpm); - } - // is there a `sharp@x.y.z` folder in the `node_modules/.pnpm` folder? - const pnpmModulesDir = path.join(this._getNextStandaloneDir(), 'node_modules', '.pnpm'); // pnpm - // get glob pattern for sharp version - const matches = glob.sync(path.join(pnpmModulesDir, 'sharp@*')); - if (matches.length > 0) { - // delete the sharp folder - if (!this.props.quiet) console.debug('├ Deleting sharp module from', matches[0]); - fs.removeSync(matches[0]); - } } // getNextBuildId() { diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index 14ceb140..3a04f95f 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -436,7 +436,6 @@ export class NextjsDistribution extends Construct { compress: true, cachePolicy: imageCachePolicy, originRequestPolicy: imageOptORP, - responseHeadersPolicy: staticResponseHeadersPolicy, }, // known static routes