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

Let CloudFront do the Gzipping #692

Merged
merged 5 commits into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ The fourth cache behaviour handles next API requests `api/*`.
| logLambdaExecutionTimes | `boolean` | `false` | Logs to CloudWatch the default handler performance metrics. |
| minifyHandlers | `boolean` | `false` | Use pre-built minified handlers to reduce code size. Does not minify custom handlers. |
| deploy | `boolean` | `true` | Whether to deploy resources to AWS. Useful if you just need the Lambdas and assets but want to deploy them yourself (available in latest alpha). |
| enableHTTPCompression | `boolean` | `false` | When set to `true` the Lambda@Edge functions for SSR and API requests will use Gzip to compress the response. Note that you shouldn't need to enable this because CloudFront will compress responses for you out of the box. |

Custom inputs can be configured like this:

Expand Down
10 changes: 10 additions & 0 deletions packages/compat-layers/lambda-at-edge-compat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,13 @@ module.exports.render = async (event, context) => {
return responsePromise;
};
```

## Options

### Gzip compression

```js
const { req, res, responsePromise } = cloudFrontCompat(event.Records[0].cf, {
enableHTTPCompression: true // false by default
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -371,11 +371,10 @@ describe("Response Tests", () => {
});
});

it(`gzips`, () => {
expect.assertions(2);
it("does not gzip by default", () => {
expect.assertions(3);

const gzipSpy = jest.spyOn(zlib, "gzipSync");
gzipSpy.mockReturnValueOnce(Buffer.from("ok-gzipped"));

const { res, responsePromise } = create({
request: {
Expand All @@ -393,6 +392,40 @@ describe("Response Tests", () => {

res.end("ok");

return responsePromise.then((response) => {
expect(gzipSpy).not.toBeCalled();
expect(response.headers["content-encoding"]).not.toBeDefined();
expect(response.body).toEqual("b2s=");
});
});

it(`gzips when compression is enabled`, () => {
expect.assertions(2);

const gzipSpy = jest.spyOn(zlib, "gzipSync");
gzipSpy.mockReturnValueOnce(Buffer.from("ok-gzipped"));

const { res, responsePromise } = create(
{
request: {
path: "/",
headers: {
"accept-encoding": [
{
key: "Accept-Encoding",
value: "gzip"
}
]
}
}
},
{
enableHTTPCompression: true
}
);

res.end("ok");

gzipSpy.mockRestore();

return responsePromise.then((response) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { CloudFrontResultResponse, CloudFrontRequest } from "aws-lambda";
import { IncomingMessage, ServerResponse } from "http";

declare function lambdaAtEdgeCompat(event: {
request: CloudFrontRequest;
}): {
type CompatOptions = {
enableHTTPCompression: boolean;
};

declare function lambdaAtEdgeCompat(
event: {
request: CloudFrontRequest;
},
options: CompatOptions
): {
responsePromise: Promise<CloudFrontResultResponse>;
req: IncomingMessage;
res: ServerResponse;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ const isGzipSupported = (headers) => {
return gz;
};

const handler = (event) => {
const defaultOptions = {
enableHTTPCompression: false
};

const handler = (event, { enableHTTPCompression } = defaultOptions) => {
const { request: cfRequest, response: cfResponse = { headers: {} } } = event;

const response = {
Expand Down Expand Up @@ -227,7 +231,7 @@ const handler = (event) => {
]);
};

let gz = isGzipSupported(headers);
let shouldGzip = enableHTTPCompression && isGzipSupported(headers);

const responsePromise = new Promise((resolve) => {
res.end = (text) => {
Expand All @@ -245,14 +249,14 @@ const handler = (event) => {

if (response.body) {
response.bodyEncoding = "base64";
response.body = gz
response.body = shouldGzip
? zlib.gzipSync(response.body).toString("base64")
: Buffer.from(response.body).toString("base64");
}

response.headers = toCloudFrontHeaders(res.headers, cfResponse.headers);

if (gz) {
if (shouldGzip) {
response.headers["content-encoding"] = [
{ key: "Content-Encoding", value: "gzip" }
];
Expand Down
6 changes: 6 additions & 0 deletions packages/e2e-tests/next-app/cypress/integration/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ describe("Pages Tests", () => {
(method) => {
it(`allows HTTP method for path ${path}: ${method}`, () => {
cy.request({ url: path, method: method }).then((response) => {
if (method !== "HEAD") {
cy.verifyResponseIsCompressed(response);
}
expect(response.status).to.equal(200);
});
});
Expand Down Expand Up @@ -46,6 +49,9 @@ describe("Pages Tests", () => {
(method) => {
it(`allows HTTP method for path ${path}: ${method}`, () => {
cy.request({ url: path, method: method }).then((response) => {
if (method !== "HEAD") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add for public files etc. but can be done later.

cy.verifyResponseIsCompressed(response);
}
expect(response.status).to.equal(200);
});
});
Expand Down
10 changes: 10 additions & 0 deletions packages/e2e-tests/test-utils/cypress/custom-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ declare namespace Cypress {
response: Cypress.Response,
shouldBeCached: boolean
) => Cypress.Chainable<JQuery>;
verifyResponseIsCompressed: (
response: Cypress.Response
) => Cypress.Chainable<JQuery>;
}
}

Expand Down Expand Up @@ -140,3 +143,10 @@ Cypress.Commands.add(
}
}
);

Cypress.Commands.add(
"verifyResponseIsCompressed",
(response: Cypress.Response) => {
expect(response.headers["content-encoding"]).to.equal("gzip");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, thanks for adding a test in the e2e tests as well.

}
);
6 changes: 4 additions & 2 deletions packages/libs/lambda-at-edge/src/api-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
OriginRequestEvent,
RoutesManifest
} from "../types";
import { CloudFrontResultResponse, CloudFrontRequest } from "aws-lambda";
import { CloudFrontResultResponse } from "aws-lambda";
import {
createRedirectResponse,
getDomainRedirectPath,
Expand Down Expand Up @@ -103,7 +103,9 @@ export const handler = async (

// eslint-disable-next-line
const page = require(`./${pagePath}`);
const { req, res, responsePromise } = cloudFrontCompat(event.Records[0].cf);
const { req, res, responsePromise } = cloudFrontCompat(event.Records[0].cf, {
enableHTTPCompression: buildManifest.enableHTTPCompression
});

page.default(req, res);

Expand Down
13 changes: 9 additions & 4 deletions packages/libs/lambda-at-edge/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type BuildOptions = {
logLambdaExecutionTimes?: boolean;
domainRedirects?: { [key: string]: string };
minifyHandlers?: boolean;
enableHTTPCompression?: boolean;
handler?: string;
};

Expand All @@ -46,7 +47,8 @@ const defaultBuildOptions = {
useServerlessTraceTarget: false,
logLambdaExecutionTimes: false,
domainRedirects: {},
minifyHandlers: false
minifyHandlers: false,
enableHTTPCompression: true
};

class Builder {
Expand Down Expand Up @@ -382,7 +384,8 @@ class Builder {
);
const {
logLambdaExecutionTimes = false,
domainRedirects = {}
domainRedirects = {},
enableHTTPCompression = false
} = this.buildOptions;

this.normalizeDomainRedirects(domainRedirects);
Expand All @@ -402,15 +405,17 @@ class Builder {
},
publicFiles: {},
trailingSlash: false,
domainRedirects: domainRedirects
domainRedirects: domainRedirects,
enableHTTPCompression
};

const apiBuildManifest: OriginRequestApiHandlerManifest = {
apis: {
dynamic: {},
nonDynamic: {}
},
domainRedirects: domainRedirects
domainRedirects: domainRedirects,
enableHTTPCompression
};

const ssrPages = defaultBuildManifest.pages.ssr;
Expand Down
12 changes: 10 additions & 2 deletions packages/libs/lambda-at-edge/src/default-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,12 @@ const handleOriginRequest = async ({
log("require JS execution time", tBeforePageRequire, tAfterPageRequire);

const tBeforeSSR = now();
const { req, res, responsePromise } = lambdaAtEdgeCompat(event.Records[0].cf);
const { req, res, responsePromise } = lambdaAtEdgeCompat(
event.Records[0].cf,
{
enableHTTPCompression: manifest.enableHTTPCompression
}
);
try {
// If page is _error.js, set status to 404 so _error.js will render a 404 page
if (pagePath === "pages/_error.js") {
Expand Down Expand Up @@ -466,7 +471,10 @@ const handleOriginResponse = async ({
// eslint-disable-next-line
const page = require(`./${pagePath}`);
const { req, res, responsePromise } = lambdaAtEdgeCompat(
event.Records[0].cf
event.Records[0].cf,
{
enableHTTPCompression: manifest.enableHTTPCompression
}
);
const isSSG = !!page.getStaticProps;
const { renderOpts, html } = await page.renderReqToHTML(
Expand Down
2 changes: 2 additions & 0 deletions packages/libs/lambda-at-edge/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type OriginRequestApiHandlerManifest = {
domainRedirects: {
[key: string]: string;
};
enableHTTPCompression: boolean;
};

export type OriginRequestDefaultHandlerManifest = {
Expand All @@ -44,6 +45,7 @@ export type OriginRequestDefaultHandlerManifest = {
[key: string]: string;
};
trailingSlash: boolean;
enableHTTPCompression: boolean;
domainRedirects: {
[key: string]: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ class NextjsComponent extends Component {
logLambdaExecutionTimes: inputs.logLambdaExecutionTimes || false,
domainRedirects: inputs.domainRedirects || {},
minifyHandlers: inputs.minifyHandlers || false,
enableHTTPCompression: false,
handler: inputs.handler
? `${inputs.handler.split(".")[0]}.js`
: undefined
Expand Down
1 change: 1 addition & 0 deletions packages/serverless-components/nextjs-component/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type ServerlessComponentInputs = {
minifyHandlers?: boolean;
uploadStaticAssetsFromBuild?: boolean;
deploy?: boolean;
enableHTTPCompression?: boolean;
};

type CloudfrontOptions = Record<string, any>;
Expand Down