Skip to content

Commit

Permalink
chore: pr feedback; fix cache-control headers
Browse files Browse the repository at this point in the history
  • Loading branch information
khuezy committed Dec 5, 2022
1 parent f098f57 commit e88ff05
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 47 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 18 additions & 22 deletions assets/lambda/ImageOptimization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 `/`
Expand All @@ -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)

}
}

Expand All @@ -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) {
Expand All @@ -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,
}
}
Expand Down
7 changes: 4 additions & 3 deletions assets/lambda/NextJsHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand All @@ -28,15 +28,16 @@ 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',
port: Number(process.env.PORT) || 3000,
// 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,
Expand Down
7 changes: 6 additions & 1 deletion src/ImageOptimizationLambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Expand Down
19 changes: 0 additions & 19 deletions src/NextjsBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 `[email protected]` 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() {
Expand Down
1 change: 0 additions & 1 deletion src/NextjsDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,6 @@ export class NextjsDistribution extends Construct {
compress: true,
cachePolicy: imageCachePolicy,
originRequestPolicy: imageOptORP,
responseHeadersPolicy: staticResponseHeadersPolicy,
},

// known static routes
Expand Down

0 comments on commit e88ff05

Please sign in to comment.