Skip to content

Commit 997b392

Browse files
sommeeeerconico974
andauthored
add aws-lambda-compressed (#819)
* add aws-lambda-compressed * changeset * review * review * lowercase --------- Co-authored-by: conico974 <[email protected]>
1 parent e9b37fd commit 997b392

File tree

7 files changed

+148
-25
lines changed

7 files changed

+148
-25
lines changed

.changeset/bright-bulldogs-laugh.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
Add aws-lambda-compressed wrapper
6+
7+
New wrapper called `aws-lambda-compressed`. The compression quality for brotli can be configured using the `BROTLI_QUALITY` environment variable. If not set, it defaults to 6.

packages/open-next/src/build/validateConfig.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const compatibilityMatrix: Record<IncludedWrapper, IncludedConverter[]> = {
1515
"aws-cloudfront",
1616
"sqs-revalidate",
1717
],
18+
"aws-lambda-compressed": ["aws-apigw-v2"],
1819
"aws-lambda-streaming": ["aws-apigw-v2"],
1920
cloudflare: ["edge"],
2021
"cloudflare-edge": ["edge"],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Readable, type Transform, Writable } from "node:stream";
2+
import type { ReadableStream } from "node:stream/web";
3+
import zlib from "node:zlib";
4+
5+
import type { AwsLambdaEvent, AwsLambdaReturn } from "types/aws-lambda";
6+
import type { InternalResult, StreamCreator } from "types/open-next";
7+
import type { WrapperHandler } from "types/overrides";
8+
import { error } from "../../adapters/logger";
9+
import { formatWarmerResponse } from "./aws-lambda";
10+
11+
const handler: WrapperHandler =
12+
async (handler, converter) =>
13+
async (event: AwsLambdaEvent): Promise<AwsLambdaReturn> => {
14+
// Handle warmer event
15+
if ("type" in event) {
16+
return formatWarmerResponse(event);
17+
}
18+
19+
const internalEvent = await converter.convertFrom(event);
20+
// This is a workaround
21+
// https://github.com/opennextjs/opennextjs-aws/blob/e9b37fd44eb856eb8ae73168bf455ff85dd8b285/packages/open-next/src/overrides/wrappers/aws-lambda.ts#L49-L53
22+
const fakeStream: StreamCreator = {
23+
writeHeaders: () => {
24+
return new Writable({
25+
write: (_chunk, _encoding, callback) => {
26+
callback();
27+
},
28+
});
29+
},
30+
};
31+
32+
const handlerResponse = await handler(internalEvent, {
33+
streamCreator: fakeStream,
34+
});
35+
36+
// Check if response is already compressed
37+
// The handlers response headers are lowercase
38+
const alreadyEncoded = handlerResponse.headers["content-encoding"] ?? "";
39+
40+
// Return early here if the response is already compressed
41+
if (alreadyEncoded) {
42+
return converter.convertTo(handlerResponse, event);
43+
}
44+
45+
// We compress the body if the client accepts it
46+
const acceptEncoding =
47+
internalEvent.headers["accept-encoding"] ??
48+
internalEvent.headers["Accept-Encoding"] ??
49+
"";
50+
51+
let contentEncoding: string | null = null;
52+
if (acceptEncoding?.includes("br")) {
53+
contentEncoding = "br";
54+
} else if (acceptEncoding?.includes("gzip")) {
55+
contentEncoding = "gzip";
56+
} else if (acceptEncoding?.includes("deflate")) {
57+
contentEncoding = "deflate";
58+
}
59+
60+
const response: InternalResult = {
61+
...handlerResponse,
62+
body: compressBody(handlerResponse.body, contentEncoding),
63+
headers: {
64+
...handlerResponse.headers,
65+
...(contentEncoding ? { "content-encoding": contentEncoding } : {}),
66+
},
67+
isBase64Encoded: !!contentEncoding || handlerResponse.isBase64Encoded,
68+
};
69+
70+
return converter.convertTo(response, event);
71+
};
72+
73+
export default {
74+
wrapper: handler,
75+
name: "aws-lambda-compressed",
76+
supportStreaming: false,
77+
};
78+
79+
function compressBody(body: ReadableStream, encoding: string | null) {
80+
// If no encoding is specified, return original body
81+
if (!encoding) return body;
82+
try {
83+
const readable = Readable.fromWeb(body);
84+
let transform: Transform;
85+
86+
switch (encoding) {
87+
case "br":
88+
transform = zlib.createBrotliCompress({
89+
params: {
90+
// This is a compromise between speed and compression ratio.
91+
// The default one will most likely timeout an AWS Lambda with default configuration on large bodies (>6mb).
92+
// Therefore we set it to 6, which is a good compromise.
93+
[zlib.constants.BROTLI_PARAM_QUALITY]:
94+
Number(process.env.BROTLI_QUALITY) ?? 6,
95+
},
96+
});
97+
break;
98+
case "gzip":
99+
transform = zlib.createGzip();
100+
break;
101+
case "deflate":
102+
transform = zlib.createDeflate();
103+
break;
104+
default:
105+
return body;
106+
}
107+
return Readable.toWeb(readable.pipe(transform));
108+
} catch (e) {
109+
error("Error compressing body:", e);
110+
// Fall back to no compression on error
111+
return body;
112+
}
113+
}

packages/open-next/src/overrides/wrappers/aws-lambda.ts

+3-23
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,14 @@
11
import { Writable } from "node:stream";
22

3-
import type {
4-
APIGatewayProxyEvent,
5-
APIGatewayProxyEventV2,
6-
APIGatewayProxyResult,
7-
APIGatewayProxyResultV2,
8-
CloudFrontRequestEvent,
9-
CloudFrontRequestResult,
10-
} from "aws-lambda";
11-
import type { WrapperHandler } from "types/overrides";
12-
3+
import type { AwsLambdaEvent, AwsLambdaReturn } from "types/aws-lambda";
134
import type { StreamCreator } from "types/open-next";
5+
import type { WrapperHandler } from "types/overrides";
146
import type {
157
WarmerEvent,
168
WarmerResponse,
179
} from "../../adapters/warmer-function";
1810

19-
type AwsLambdaEvent =
20-
| APIGatewayProxyEventV2
21-
| CloudFrontRequestEvent
22-
| APIGatewayProxyEvent
23-
| WarmerEvent;
24-
25-
type AwsLambdaReturn =
26-
| APIGatewayProxyResultV2
27-
| APIGatewayProxyResult
28-
| CloudFrontRequestResult
29-
| WarmerResponse;
30-
31-
function formatWarmerResponse(event: WarmerEvent) {
11+
export function formatWarmerResponse(event: WarmerEvent) {
3212
return new Promise<WarmerResponse>((resolve) => {
3313
setTimeout(() => {
3414
resolve({ serverId, type: "warmer" } satisfies WarmerResponse);

packages/open-next/src/types/aws-lambda.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import type { Writable } from "node:stream";
2-
import type { APIGatewayProxyEventV2, Context } from "aws-lambda";
2+
3+
import type {
4+
APIGatewayProxyEvent,
5+
APIGatewayProxyEventV2,
6+
APIGatewayProxyResult,
7+
APIGatewayProxyResultV2,
8+
CloudFrontRequestEvent,
9+
CloudFrontRequestResult,
10+
Context,
11+
} from "aws-lambda";
12+
import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";
313

414
export interface ResponseStream extends Writable {
515
getBufferedData(): Buffer;
@@ -25,3 +35,15 @@ declare global {
2535
}
2636
}
2737
}
38+
39+
export type AwsLambdaEvent =
40+
| APIGatewayProxyEventV2
41+
| CloudFrontRequestEvent
42+
| APIGatewayProxyEvent
43+
| WarmerEvent;
44+
45+
export type AwsLambdaReturn =
46+
| APIGatewayProxyResultV2
47+
| APIGatewayProxyResult
48+
| CloudFrontRequestResult
49+
| WarmerResponse;

packages/open-next/src/types/open-next.ts

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export interface Origin {
9494
export type IncludedWrapper =
9595
| "aws-lambda"
9696
| "aws-lambda-streaming"
97+
| "aws-lambda-compressed"
9798
| "node"
9899
// @deprecated - use "cloudflare-edge" instead.
99100
| "cloudflare"

packages/open-next/src/types/overrides.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { Readable } from "node:stream";
22

33
import type { Meta } from "types/cache";
4-
54
import type {
65
BaseEventOrResult,
76
BaseOverride,

0 commit comments

Comments
 (0)