Skip to content

add aws-lambda-compressed #819

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 10, 2025
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
7 changes: 7 additions & 0 deletions .changeset/bright-bulldogs-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@opennextjs/aws": patch
---

Add aws-lambda-compressed wrapper

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.
1 change: 1 addition & 0 deletions packages/open-next/src/build/validateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const compatibilityMatrix: Record<IncludedWrapper, IncludedConverter[]> = {
"aws-cloudfront",
"sqs-revalidate",
],
"aws-lambda-compressed": ["aws-apigw-v2"],
"aws-lambda-streaming": ["aws-apigw-v2"],
cloudflare: ["edge"],
"cloudflare-edge": ["edge"],
Expand Down
113 changes: 113 additions & 0 deletions packages/open-next/src/overrides/wrappers/aws-lambda-compressed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Readable, type Transform, Writable } from "node:stream";
import type { ReadableStream } from "node:stream/web";
import zlib from "node:zlib";

import type { AwsLambdaEvent, AwsLambdaReturn } from "types/aws-lambda";
import type { InternalResult, StreamCreator } from "types/open-next";
import type { WrapperHandler } from "types/overrides";
import { error } from "../../adapters/logger";
import { formatWarmerResponse } from "./aws-lambda";

const handler: WrapperHandler =
async (handler, converter) =>
async (event: AwsLambdaEvent): Promise<AwsLambdaReturn> => {
// Handle warmer event
if ("type" in event) {
return formatWarmerResponse(event);
}

const internalEvent = await converter.convertFrom(event);
// This is a workaround
// https://github.com/opennextjs/opennextjs-aws/blob/e9b37fd44eb856eb8ae73168bf455ff85dd8b285/packages/open-next/src/overrides/wrappers/aws-lambda.ts#L49-L53
const fakeStream: StreamCreator = {
writeHeaders: () => {
return new Writable({
write: (_chunk, _encoding, callback) => {
callback();
},
});
},
};

const handlerResponse = await handler(internalEvent, {
streamCreator: fakeStream,
});

// Check if response is already compressed
// The handlers response headers are lowercase
const alreadyEncoded = handlerResponse.headers["content-encoding"] ?? "";

// Return early here if the response is already compressed
if (alreadyEncoded) {
return converter.convertTo(handlerResponse, event);
}

// We compress the body if the client accepts it
const acceptEncoding =
internalEvent.headers["accept-encoding"] ??
internalEvent.headers["Accept-Encoding"] ??
"";

let contentEncoding: string | null = null;
if (acceptEncoding?.includes("br")) {
contentEncoding = "br";
} else if (acceptEncoding?.includes("gzip")) {
contentEncoding = "gzip";
} else if (acceptEncoding?.includes("deflate")) {
contentEncoding = "deflate";
}

const response: InternalResult = {
...handlerResponse,
body: compressBody(handlerResponse.body, contentEncoding),
headers: {
...handlerResponse.headers,
...(contentEncoding ? { "content-encoding": contentEncoding } : {}),
},
isBase64Encoded: !!contentEncoding || handlerResponse.isBase64Encoded,
};

return converter.convertTo(response, event);
};

export default {
wrapper: handler,
name: "aws-lambda-compressed",
supportStreaming: false,
};

function compressBody(body: ReadableStream, encoding: string | null) {
// If no encoding is specified, return original body
if (!encoding) return body;
try {
const readable = Readable.fromWeb(body);
let transform: Transform;

switch (encoding) {
case "br":
transform = zlib.createBrotliCompress({
params: {
// This is a compromise between speed and compression ratio.
// The default one will most likely timeout an AWS Lambda with default configuration on large bodies (>6mb).
// Therefore we set it to 6, which is a good compromise.
[zlib.constants.BROTLI_PARAM_QUALITY]:
Number(process.env.BROTLI_QUALITY) ?? 6,
},
});
break;
case "gzip":
transform = zlib.createGzip();
break;
case "deflate":
transform = zlib.createDeflate();
break;
default:
return body;
}
return Readable.toWeb(readable.pipe(transform));
} catch (e) {
error("Error compressing body:", e);
// Fall back to no compression on error
return body;
}
}
26 changes: 3 additions & 23 deletions packages/open-next/src/overrides/wrappers/aws-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,14 @@
import { Writable } from "node:stream";

import type {
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
APIGatewayProxyResultV2,
CloudFrontRequestEvent,
CloudFrontRequestResult,
} from "aws-lambda";
import type { WrapperHandler } from "types/overrides";

import type { AwsLambdaEvent, AwsLambdaReturn } from "types/aws-lambda";
import type { StreamCreator } from "types/open-next";
import type { WrapperHandler } from "types/overrides";
import type {
WarmerEvent,
WarmerResponse,
} from "../../adapters/warmer-function";

type AwsLambdaEvent =
| APIGatewayProxyEventV2
| CloudFrontRequestEvent
| APIGatewayProxyEvent
| WarmerEvent;

type AwsLambdaReturn =
| APIGatewayProxyResultV2
| APIGatewayProxyResult
| CloudFrontRequestResult
| WarmerResponse;

function formatWarmerResponse(event: WarmerEvent) {
export function formatWarmerResponse(event: WarmerEvent) {
return new Promise<WarmerResponse>((resolve) => {
setTimeout(() => {
resolve({ serverId, type: "warmer" } satisfies WarmerResponse);
Expand Down
24 changes: 23 additions & 1 deletion packages/open-next/src/types/aws-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type { Writable } from "node:stream";
import type { APIGatewayProxyEventV2, Context } from "aws-lambda";

import type {
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
APIGatewayProxyResultV2,
CloudFrontRequestEvent,
CloudFrontRequestResult,
Context,
} from "aws-lambda";
import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";

export interface ResponseStream extends Writable {
getBufferedData(): Buffer;
Expand All @@ -25,3 +35,15 @@ declare global {
}
}
}

export type AwsLambdaEvent =
| APIGatewayProxyEventV2
| CloudFrontRequestEvent
| APIGatewayProxyEvent
| WarmerEvent;

export type AwsLambdaReturn =
| APIGatewayProxyResultV2
| APIGatewayProxyResult
| CloudFrontRequestResult
| WarmerResponse;
1 change: 1 addition & 0 deletions packages/open-next/src/types/open-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface Origin {
export type IncludedWrapper =
| "aws-lambda"
| "aws-lambda-streaming"
| "aws-lambda-compressed"
| "node"
// @deprecated - use "cloudflare-edge" instead.
| "cloudflare"
Expand Down
1 change: 0 additions & 1 deletion packages/open-next/src/types/overrides.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Readable } from "node:stream";

import type { Meta } from "types/cache";

import type {
BaseEventOrResult,
BaseOverride,
Expand Down
Loading