Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 { InternalResult, StreamCreator } from "types/open-next";
import type {
AwsLambdaEvent,
AwsLambdaReturn,
WrapperHandler,
} from "types/overrides";
import { formatWarmerResponse } from "utils/overrides";
import { error } from "../../adapters/logger";

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, you can read more about it in the aws-lambda wrapper
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
const prevEncoding =
handlerResponse.headers?.["content-encoding"] ??
handlerResponse.headers?.["Content-Encoding"] ??
"";

// Return early here if the response is already compressed

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;
}
}
38 changes: 5 additions & 33 deletions packages/open-next/src/overrides/wrappers/aws-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
import { Writable } from "node:stream";

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

import type { StreamCreator } from "types/open-next";
import type {
WarmerEvent,
WarmerResponse,
} from "../../adapters/warmer-function";

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

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

function formatWarmerResponse(event: WarmerEvent) {
return new Promise<WarmerResponse>((resolve) => {
setTimeout(() => {
resolve({ serverId, type: "warmer" } satisfies WarmerResponse);
}, event.delay);
});
}
AwsLambdaEvent,
AwsLambdaReturn,
WrapperHandler,
} from "types/overrides";
import { formatWarmerResponse } from "utils/overrides";

const handler: WrapperHandler =
async (handler, converter) =>
Expand Down
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
21 changes: 21 additions & 0 deletions packages/open-next/src/types/overrides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import type { Readable } from "node:stream";

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

import type {
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
APIGatewayProxyResultV2,
CloudFrontRequestEvent,
CloudFrontRequestResult,
} from "aws-lambda";
import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";
import type {
BaseEventOrResult,
BaseOverride,
Expand Down Expand Up @@ -229,3 +238,15 @@ type CDNPath = {
export type CDNInvalidationHandler = BaseOverride & {
invalidatePaths: (paths: CDNPath[]) => Promise<void>;
};

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

export type AwsLambdaReturn =
| APIGatewayProxyResultV2
| APIGatewayProxyResult
| CloudFrontRequestResult
| WarmerResponse;
9 changes: 9 additions & 0 deletions packages/open-next/src/utils/overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { WarmerEvent, WarmerResponse } from "../adapters/warmer-function";

export function formatWarmerResponse(event: WarmerEvent) {
return new Promise<WarmerResponse>((resolve) => {
setTimeout(() => {
resolve({ serverId, type: "warmer" } satisfies WarmerResponse);
}, event.delay);
});
}
Loading