From 614e87e9f03fb1659fb038aee7fe6ecbb4660fdc Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 7 Feb 2022 12:25:05 +0000 Subject: [PATCH] feat(nitro): add support for lambda v2 payload format (#3070) Co-authored-by: Pooya Parsa --- .../3.docs/3.deployment/99.presets/lambda.md | 6 +++- packages/nitro/package.json | 1 + packages/nitro/src/runtime/entries/lambda.ts | 23 +++++++++--- test/presets/lambda.test.ts | 35 +++++++++++++++++-- yarn.lock | 8 +++++ 5 files changed, 65 insertions(+), 8 deletions(-) diff --git a/docs/content/3.docs/3.deployment/99.presets/lambda.md b/docs/content/3.docs/3.deployment/99.presets/lambda.md index 7c8e7e47b6..f447373324 100644 --- a/docs/content/3.docs/3.deployment/99.presets/lambda.md +++ b/docs/content/3.docs/3.deployment/99.presets/lambda.md @@ -24,6 +24,10 @@ Or directly use the `NITRO_PRESET` environment variable when running `nuxt build NITRO_PRESET=lambda npx nuxt build ``` +::alert +AWS Lambda [defaults to payload version v2](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). This Nitro preset supports both v1 and v2 payloads. +:: + ### Entrypoint When running `nuxt build` with the Lambda preset, the result will be an entry point that exports a handler function that responds to an event and returns a response. @@ -36,5 +40,5 @@ It can be used programmatically or as part of a deployment. import { handler } from './.output/server' // Use programmatically -const { statusCode, headers, body } = handler({ path: '/' }) +const { statusCode, headers, body } = handler({ rawPath: '/' }) ``` diff --git a/packages/nitro/package.json b/packages/nitro/package.json index ba21a4d12c..d7f98ed9ce 100644 --- a/packages/nitro/package.json +++ b/packages/nitro/package.json @@ -73,6 +73,7 @@ }, "devDependencies": { "@nuxt/schema": "3.0.0", + "@types/aws-lambda": "^8.10.92", "@types/fs-extra": "^9.0.13", "@types/http-proxy": "^1.17.8", "@types/node-fetch": "^3.0.2", diff --git a/packages/nitro/src/runtime/entries/lambda.ts b/packages/nitro/src/runtime/entries/lambda.ts index 116b58c21c..e1e69c3f56 100644 --- a/packages/nitro/src/runtime/entries/lambda.ts +++ b/packages/nitro/src/runtime/entries/lambda.ts @@ -1,21 +1,34 @@ +import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, APIGatewayProxyEventV2, Context } from 'aws-lambda' import '#polyfill' import { withQuery } from 'ufo' +import type { HeadersObject } from 'unenv/runtime/_internal/types' import { localCall } from '../server' -export async function handler (event, context) { +export const handler = async function handler (event: APIGatewayProxyEvent & APIGatewayProxyEventV2, context: Context) { + const url = withQuery(event.path || event.rawPath, event.queryStringParameters) + const method = event.httpMethod || event.requestContext?.http?.method || 'get' + const r = await localCall({ event, - url: withQuery(event.path, event.queryStringParameters), + url, context, - headers: event.headers, - method: event.httpMethod, + headers: normalizeIncomingHeaders(event.headers), + method, query: event.queryStringParameters, body: event.body // TODO: handle event.isBase64Encoded }) return { statusCode: r.status, - headers: r.headers, + headers: normalizeOutgoingHeaders(r.headers), body: r.body.toString() } } + +function normalizeIncomingHeaders (headers: APIGatewayProxyEventHeaders) { + return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value as string])) +} + +function normalizeOutgoingHeaders (headers: HeadersObject) { + return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : v])) +} diff --git a/test/presets/lambda.test.ts b/test/presets/lambda.test.ts index e630defeef..f75db2bd08 100644 --- a/test/presets/lambda.test.ts +++ b/test/presets/lambda.test.ts @@ -1,19 +1,50 @@ import { resolve } from 'pathe' import { describe } from 'vitest' +import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda' import { setupTest, testNitroBehavior, importModule } from './_tests' describe('nitro:preset:lambda', () => { const ctx = setupTest('lambda') + // Lambda v1 paylod testNitroBehavior(ctx, async () => { const { handler } = await importModule(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}`) const queryStringParameters = Object.fromEntries(url.searchParams.entries()) - const event = { + const event: Partial = { + resource: '/my/path', path: url.pathname, headers: headers || {}, - method: method || 'GET', + httpMethod: method || 'GET', + queryStringParameters, + body: body || '' + } + const res = await handler(event) + return { + data: res.body + } + } + }) + // Lambda v2 paylod + testNitroBehavior(ctx, async () => { + const { handler } = await importModule(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}`) + const queryStringParameters = Object.fromEntries(url.searchParams.entries()) + const event: Partial = { + rawPath: url.pathname, + headers: headers || {}, + requestContext: { + ...Object.fromEntries([['accountId'], ['apiId'], ['domainName'], ['domainPrefix']]), + http: { + path: url.pathname, + protocol: 'http', + ...Object.fromEntries([['userAgent'], ['sourceIp']]), + method: method || 'GET' + } + }, queryStringParameters, body: body || '' } diff --git a/yarn.lock b/yarn.lock index 9c18a228df..0ab33abe95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2967,6 +2967,7 @@ __metadata: "@rollup/plugin-virtual": ^2.0.3 "@rollup/plugin-wasm": ^5.1.2 "@rollup/pluginutils": ^4.1.2 + "@types/aws-lambda": ^8.10.92 "@types/fs-extra": ^9.0.13 "@types/http-proxy": ^1.17.8 "@types/jsdom": ^16.2.14 @@ -3902,6 +3903,13 @@ __metadata: languageName: node linkType: hard +"@types/aws-lambda@npm:^8.10.92": + version: 8.10.92 + resolution: "@types/aws-lambda@npm:8.10.92" + checksum: 71c44d83a1c88aa6dbc920baedfb2d100b8843a3d210c695ccaafb30dfb75f04398b0e5368100022acbf75c55d456c61774242f20dd70915fc63d85430cbcf8a + languageName: node + linkType: hard + "@types/babel__core@npm:7.1.14": version: 7.1.14 resolution: "@types/babel__core@npm:7.1.14"