Skip to content

Commit b77f08d

Browse files
conico974sommeeeer
authored andcommitted
add aws-lambda-compressed
1 parent e9b37fd commit b77f08d

File tree

3 files changed

+138
-0
lines changed

3 files changed

+138
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
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"],
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Readable, Writable } from "node:stream";
2+
import type { ReadableStream } from "node:stream/web";
3+
import zlib from "node:zlib";
4+
5+
import type {
6+
APIGatewayProxyEvent,
7+
APIGatewayProxyEventV2,
8+
APIGatewayProxyResult,
9+
APIGatewayProxyResultV2,
10+
CloudFrontRequestEvent,
11+
CloudFrontRequestResult,
12+
} from "aws-lambda";
13+
import type { WrapperHandler } from "types/overrides";
14+
15+
import type { InternalResult, StreamCreator } from "types/open-next";
16+
import { error } from "../../adapters/logger";
17+
import type {
18+
WarmerEvent,
19+
WarmerResponse,
20+
} from "../../adapters/warmer-function";
21+
22+
type AwsLambdaEvent =
23+
| APIGatewayProxyEventV2
24+
| CloudFrontRequestEvent
25+
| APIGatewayProxyEvent
26+
| WarmerEvent;
27+
28+
type AwsLambdaReturn =
29+
| APIGatewayProxyResultV2
30+
| APIGatewayProxyResult
31+
| CloudFrontRequestResult
32+
| WarmerResponse;
33+
34+
function formatWarmerResponse(event: WarmerEvent) {
35+
return new Promise<WarmerResponse>((resolve) => {
36+
setTimeout(() => {
37+
resolve({ serverId, type: "warmer" } satisfies WarmerResponse);
38+
}, event.delay);
39+
});
40+
}
41+
42+
const handler: WrapperHandler =
43+
async (handler, converter) =>
44+
async (event: AwsLambdaEvent): Promise<AwsLambdaReturn> => {
45+
// Handle warmer event
46+
if ("type" in event) {
47+
return formatWarmerResponse(event);
48+
}
49+
50+
const internalEvent = await converter.convertFrom(event);
51+
//TODO: create a simple reproduction and open an issue in the node repo
52+
//This is a workaround, there is an issue in node that causes node to crash silently if the OpenNextNodeResponse stream is not consumed
53+
//This does not happen everytime, it's probably caused by suspended component in ssr (either via <Suspense> or loading.tsx)
54+
//Everyone that wish to create their own wrapper without a StreamCreator should implement this workaround
55+
//This is not necessary if the underlying handler does not use OpenNextNodeResponse (At the moment, OpenNextNodeResponse is used by the node runtime servers and the image server)
56+
const fakeStream: StreamCreator = {
57+
writeHeaders: () => {
58+
return new Writable({
59+
write: (_chunk, _encoding, callback) => {
60+
callback();
61+
},
62+
});
63+
},
64+
};
65+
66+
const acceptEncoding =
67+
internalEvent.headers["accept-encoding"] ??
68+
internalEvent.headers["Accept-Encoding"] ??
69+
"";
70+
71+
let contentEncoding: string | null = null;
72+
if (acceptEncoding?.includes("br")) {
73+
contentEncoding = "br";
74+
} else if (acceptEncoding?.includes("gzip")) {
75+
contentEncoding = "gzip";
76+
} else if (acceptEncoding?.includes("deflate")) {
77+
contentEncoding = "deflate";
78+
}
79+
80+
const handlerResponse = await handler(internalEvent, {
81+
streamCreator: fakeStream,
82+
});
83+
84+
const response: InternalResult = {
85+
...handlerResponse,
86+
body: compressBody(handlerResponse.body, contentEncoding),
87+
headers: {
88+
...handlerResponse.headers,
89+
...(contentEncoding ? { "content-encoding": contentEncoding } : {}),
90+
},
91+
isBase64Encoded: !!contentEncoding || handlerResponse.isBase64Encoded,
92+
};
93+
94+
return converter.convertTo(response, event);
95+
};
96+
97+
export default {
98+
wrapper: handler,
99+
name: "aws-lambda-compressed",
100+
supportStreaming: false,
101+
};
102+
103+
function compressBody(body: ReadableStream, encoding: string | null) {
104+
// If no encoding is specified, return original body
105+
if (!encoding) return body;
106+
try {
107+
const readable = Readable.fromWeb(body);
108+
109+
switch (encoding) {
110+
case "br":
111+
return Readable.toWeb(
112+
readable.pipe(
113+
zlib.createBrotliCompress({
114+
params: {
115+
// This is a compromise between speed and compression ratio.
116+
// The default one will most likely timeout an AWS Lambda with default configuration on large bodies (>6mb).
117+
// Therefore we set it to 6, which is a good compromise.
118+
[zlib.constants.BROTLI_PARAM_QUALITY]:
119+
Number(process.env.BROTLI_QUALITY) ?? 6,
120+
},
121+
}),
122+
),
123+
);
124+
case "gzip":
125+
return Readable.toWeb(readable.pipe(zlib.createGzip()));
126+
case "deflate":
127+
return Readable.toWeb(readable.pipe(zlib.createDeflate()));
128+
default:
129+
return body;
130+
}
131+
} catch (e) {
132+
error("Error compressing body:", e);
133+
// Fall back to no compression on error
134+
return body;
135+
}
136+
}

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

Lines changed: 1 addition & 0 deletions
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"

0 commit comments

Comments
 (0)