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

feat(preset): Aws lambda edge #1075

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions docs/content/2.deploy/0.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ When running Nitro in development mode, Nitro will always use a special preset c

When deploying to the production using CI/CD, Nitro tries to automatically detect the provider environment and set the right one without any additional configuration. Currently, providers below can be auto-detected with zero config.

- [aws](/deploy/providers/aws)
- [azure](/deploy/providers/azure)
- [cloudflare_pages](/deploy/providers/cloudflare#cloudflare-pages)
- [netlify](/deploy/providers/netlify)
Expand Down
7 changes: 7 additions & 0 deletions docs/content/2.deploy/providers/aws.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ import { handler } from './.output/server'
// Use programmatically
const { statusCode, headers, body } = handler({ rawPath: '/' })
```
## AWS Lambda@Edge

**Preset:** `aws-lambda-edge` ([switch to this preset](/deploy/#changing-the-deployment-preset))

Nitro provides a built-in preset to generate output format compatible with [AWS Lambda@Edge](https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html).

The output entrypoint in `.output/server/index.mjs` is compatible with [AWS Lambda@Edge format](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html).
5 changes: 5 additions & 0 deletions src/presets/aws-lambda-edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineNitroPreset } from "../preset";

export const awsLambdaEdge = defineNitroPreset({
entry: "#internal/nitro/entries/aws-lambda-edge",
});
1 change: 1 addition & 0 deletions src/presets/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./aws-lambda";
export * from "./aws-lambda-edge";
export * from "./azure-functions";
export * from "./azure";
export * from "./base-worker";
Expand Down
75 changes: 75 additions & 0 deletions src/runtime/entries/aws-lambda-edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type {
CloudFrontHeaders,
Context,
CloudFrontRequestEvent,
CloudFrontResultResponse,
} from "aws-lambda";
import "#internal/nitro/virtual/polyfill";
import { nitroApp } from "../app";

export const handler = async function handler(
event: CloudFrontRequestEvent,
context: Context
): Promise<CloudFrontResultResponse> {
const request = event.Records[0].cf.request;
const url = getFullUrl(request.uri, request.querystring);

const r = await nitroApp.localCall({
event,
url,
context,
headers: normalizeIncomingHeaders(request.headers),
method: request.method,
query: request.querystring,
body: normalizeBody(request.body),
});

return {
status: r.status.toString(),
headers: normalizeOutgoingHeaders(r.headers),
body: r.body.toString(),
};
};

function normalizeBody(
body: CloudFrontRequestEvent["Records"][0]["cf"]["request"]["body"]
) {
if (typeof body === "undefined") {
return body;
}

const bodyString = body;
if (typeof body.encoding !== "undefined" && body.encoding === "base64") {
bodyString.data = Buffer.from(body.data, "base64").toString("utf8");
bodyString.data = decodeURIComponent(bodyString.data);
}
return bodyString;
}

function normalizeIncomingHeaders(headers: CloudFrontHeaders) {
return Object.fromEntries(
Object.entries(headers).map(([key, keyValues]) => [
key,
keyValues.map((kv) => kv.value),
])
);
}

function normalizeOutgoingHeaders(
headers: Record<string, string | string[] | undefined>
): CloudFrontHeaders {
const entries = Object.fromEntries(
Object.entries(headers).filter(([key]) => !["content-length"].includes(key))
);

return Object.fromEntries(
Object.entries(entries).map(([k, v]) => [
k,
Array.isArray(v) ? v.map((value) => ({ value })) : [{ value: v }],
])
);
}

function getFullUrl(uri: string, querystring: string | undefined) {
return uri + (querystring ? "?" + querystring : "");
}
64 changes: 64 additions & 0 deletions test/presets/aws-lambda-edge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { resolve } from "pathe";
import { describe } from "vitest";
import destr from "destr";
import type {
CloudFrontHeaders,
CloudFrontRequestEvent,
CloudFrontResultResponse,
} from "aws-lambda";
import { setupTest, testNitro } from "../tests";

describe("nitro:preset:aws-lambda-edge", async () => {
const ctx = await setupTest("aws-lambda-edge");
testNitro(ctx, async () => {
const { handler } = await import(resolve(ctx.outDir, "server/index.mjs"));
return async ({ url: rawRelativeUrl, headers, method, body }) => {
// creating new URL object to parse query easier
const url = new URL(`https://example.com${rawRelativeUrl}`);
// modify headers to CloudFrontHeaders.
const reqHeaders: CloudFrontHeaders = Object.fromEntries(
Object.entries(headers || {}).map(([k, v]) => [
k,
Array.isArray(v) ? v.map((value) => ({ value })) : [{ value: v }],
])
);

const event: CloudFrontRequestEvent = {
Records: [
{
cf: {
config: {
distributionDomainName: "nitro.cloudfront.net",
distributionId: "EDFDVBD6EXAMPLE",
eventType: "origin-request",
requestId:
"4TyzHTaYWb1GX1qTfsHhEqV6HUDd_BzoBZnwfnvQc_1oF26ClkoUSEQ==",
},
request: {
clientIp: "203.0.113.178",
method: method || "GET",
uri: url.pathname,
querystring: url.searchParams.toString(),
headers: reqHeaders,
body,
},
},
},
],
};
const res: CloudFrontResultResponse = await handler(event);
// responsed CloudFrontHeaders are special, so modify them for testing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

const resHeaders = Object.fromEntries(
Object.entries(res.headers).map(([key, keyValues]) => [
key,
keyValues.map((kv) => kv.value).join(","),
])
);
return {
data: destr(res.body),
status: Number.parseInt(res.status),
headers: resHeaders,
};
};
});
});