Skip to content
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

fea: add aws-lambda-alb support #2914

Draft
wants to merge 4 commits into
base: v2
Choose a base branch
from
Draft
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
13 changes: 12 additions & 1 deletion docs/2.deploy/20.providers/aws.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
:read-more{title="AWS Lambda" to="https://aws.amazon.com/lambda/"}

Nitro provides a built-in preset to generate output format compatible with [AWS Lambda](https://aws.amazon.com/lambda/).
The output entrypoint in `.output/server/index.mjs` is compatible with [AWS Lambda format](https://docs.aws.amazon.com/lex/latest/dg/lambda-input-response-format.html).
The output entrypoint in `.output/server/index.mjs` is an [AWS Lambda function handler](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html).

It can be used programmatically or as part of a deployment.

Expand All @@ -18,6 +18,17 @@ import { handler } from './.output/server'
const { statusCode, headers, body } = handler({ rawPath: '/' })
```

## Supported event types

The following [@types/aws-lambda](https://www.npmjs.com/package/@types/aws-lambda) event types are supported:

- `ALBEvent`: ALB (Application Load Balancer) [Lambda target group](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html).
- `APIGatewayProxyEvent`: API Gateway REST [Lambda proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html) or API Gateway HTTP [Lambda integration with 1.0 payload format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
- `APIGatewayProxyEventV2`: API Gateway HTTP [Lambda integration with 2.0 payload format](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html).
- `LambdaFunctionURLEvent`: Lambda [function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html).

_Note:_ API Gateway REST [Lambda custom integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-custom-integrations.html) are not supported.

## Inlining chunks

Nitro output, by default uses dynamic chunks for lazy loading code only when needed. However this sometimes can not be ideal for performance. (See discussions in [nitrojs/nitro#650](https://github.com/nitrojs/nitro/pull/650)). You can enabling chunk inlining behavior using [`inlineDynamicImports`](/config#inlinedynamicimports) config.
Expand Down
26 changes: 15 additions & 11 deletions src/presets/aws-lambda/runtime/aws-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type {
ALBEvent,
ALBResult,
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
Expand All @@ -10,36 +12,38 @@ import { useNitroApp } from "nitropack/runtime";
import {
normalizeCookieHeader,
normalizeLambdaIncomingHeaders,
normalizeLambdaIncomingQuery,
normalizeLambdaOutgoingBody,
normalizeLambdaOutgoingHeaders,
} from "nitropack/runtime/internal";
import { withQuery } from "ufo";

const nitroApp = useNitroApp();

export async function handler(
event: ALBEvent,
context: Context
): Promise<ALBResult>;
export async function handler(
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult>;
export async function handler(
event: APIGatewayProxyEventV2,
event: APIGatewayProxyEventV2, // `LambdaFunctionURLEvent` is an alias of `APIGatewayProxyEventV2`
context: Context
): Promise<APIGatewayProxyResultV2>;
): Promise<APIGatewayProxyResultV2>; // `LambdaFunctionURLResult` is an alias of `APIGatewayProxyResultV2`
export async function handler(
event: APIGatewayProxyEvent | APIGatewayProxyEventV2,
event: ALBEvent | APIGatewayProxyEvent | APIGatewayProxyEventV2,
context: Context
): Promise<APIGatewayProxyResult | APIGatewayProxyResultV2> {
const query = {
...event.queryStringParameters,
...(event as APIGatewayProxyEvent).multiValueQueryStringParameters,
};
): Promise<ALBResult | APIGatewayProxyResult | APIGatewayProxyResultV2> {
const query = normalizeLambdaIncomingQuery(event);
const url = withQuery(
(event as APIGatewayProxyEvent).path ||
(event as ALBEvent | APIGatewayProxyEvent).path ||
(event as APIGatewayProxyEventV2).rawPath,
query
);
const method =
(event as APIGatewayProxyEvent).httpMethod ||
(event as ALBEvent | APIGatewayProxyEvent).httpMethod ||
(event as APIGatewayProxyEventV2).requestContext?.http?.method ||
"get";

Expand All @@ -51,7 +55,7 @@ export async function handler(
event,
url,
context,
headers: normalizeLambdaIncomingHeaders(event.headers) as Record<
headers: normalizeLambdaIncomingHeaders(event) as Record<
string,
string | string[]
>,
Expand Down
1 change: 1 addition & 0 deletions src/runtime/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {

export {
normalizeLambdaIncomingHeaders,
normalizeLambdaIncomingQuery,
normalizeLambdaOutgoingHeaders,
normalizeLambdaOutgoingBody,
} from "./utils.lambda";
Expand Down
112 changes: 112 additions & 0 deletions src/runtime/internal/utils.lambda/incoming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type {
ALBEvent,
ALBEventMultiValueQueryStringParameters,
ALBEventQueryStringParameters,
APIGatewayProxyEvent,
APIGatewayProxyEventHeaders,
APIGatewayProxyEventMultiValueHeaders,
APIGatewayProxyEventV2,
} from "aws-lambda";
import { decode } from "ufo";

export function normalizeLambdaIncomingHeaders(
headers?:
| ALBEventQueryStringParameters
| ALBEventMultiValueQueryStringParameters
| APIGatewayProxyEventHeaders
| APIGatewayProxyEventMultiValueHeaders
): Record<string, string | string[] | undefined>;
export function normalizeLambdaIncomingHeaders(
event: ALBEvent | APIGatewayProxyEvent | APIGatewayProxyEventV2
): Record<string, string | string[] | undefined>;
export function normalizeLambdaIncomingHeaders(
eventOrHeaders?:
| ALBEvent
| APIGatewayProxyEvent
| APIGatewayProxyEventV2
| APIGatewayProxyEventHeaders
| APIGatewayProxyEventMultiValueHeaders
): Record<string, string | string[] | undefined> {
if (isEvent(eventOrHeaders)) {
// ALB event has either `headers` or `multiValueHeaders`
if (isAlbEvent(eventOrHeaders)) {
return normalizeLambdaIncomingHeaders(
eventOrHeaders.headers ?? eventOrHeaders.multiValueHeaders
);
}

// Other events always have `headers`
return normalizeLambdaIncomingHeaders(eventOrHeaders.headers);
}

// if we're here, `eventOrHeaders` is `APIGatewayProxyEventHeaders`
return Object.fromEntries(
Object.entries(eventOrHeaders || {}).map(([key, value]) => [
key.toLowerCase(),
value,
])
);
}

export function normalizeLambdaIncomingQuery(
event: ALBEvent | APIGatewayProxyEvent | APIGatewayProxyEventV2
): Record<string, string | string[] | undefined> {
const rawQueryObj: Record<string, string | string[] | undefined> = {
...event.queryStringParameters,
...(event as ALBEvent | APIGatewayProxyEvent)
.multiValueQueryStringParameters,
};

// `APIGatewayProxyEvent | APIGatewayProxyEventV2` have URL-decoded query parameters
if (!isAlbEvent(event)) {
return rawQueryObj;
}

/*
* `ALBEvent` has either `queryStringParameters` or `multiValueQueryStringParameters`.
* Query params in raw form, they must be decoded to avoid double URL encoding
*/
return Object.fromEntries(
Object.entries(
event.queryStringParameters ?? event.multiValueQueryStringParameters ?? {}
).map(([key, value]) => {
let decodedValue: string | string[] | undefined;
if (typeof value === "string") {
decodedValue = decode(value);
} else if (Array.isArray(value)) {
decodedValue = value.map((v) => decode(v));
} else {
decodedValue = value;
}

return [decode(key), decodedValue];
})
);
}

// -- Internal --

function isAlbEvent(
event: ALBEvent | APIGatewayProxyEvent | APIGatewayProxyEventV2
): event is ALBEvent {
return !!event?.requestContext && "elb" in event.requestContext;
}

function isEvent(
obj?:
| ALBEvent
| ALBEventQueryStringParameters
| ALBEventMultiValueQueryStringParameters
| APIGatewayProxyEvent
| APIGatewayProxyEventV2
| APIGatewayProxyEventHeaders
| APIGatewayProxyEventMultiValueHeaders
): obj is ALBEvent | APIGatewayProxyEvent | APIGatewayProxyEventV2 {
// All events have a `requestContext` object field
return (
!!obj &&
!!obj.requestContext &&
typeof obj.requestContext === "object" &&
!Array.isArray(obj.requestContext)
);
}
2 changes: 2 additions & 0 deletions src/runtime/internal/utils.lambda/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./incoming";
export * from "./outgoing";
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import type { Readable } from "node:stream";
import type { APIGatewayProxyEventHeaders } from "aws-lambda";
import { toBuffer } from "./utils";

export function normalizeLambdaIncomingHeaders(
headers?: APIGatewayProxyEventHeaders
): Record<string, string | string[] | undefined> {
return Object.fromEntries(
Object.entries(headers || {}).map(([key, value]) => [
key.toLowerCase(),
value,
])
);
}
import { toBuffer } from "../utils";

export function normalizeLambdaOutgoingHeaders(
headers: Record<string, number | string | string[] | undefined>,
Expand Down
Loading