diff --git a/src/presets/aws-lambda/runtime/_utils.ts b/src/presets/aws-lambda/runtime/_utils.ts deleted file mode 100644 index a5efd6cdbf..0000000000 --- a/src/presets/aws-lambda/runtime/_utils.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from "aws-lambda"; -import type { ServerRequest } from "srvx"; -import { stringifyQuery } from "ufo"; - -// Incoming (AWS => Web) - -export function awsRequest( - event: APIGatewayProxyEvent | APIGatewayProxyEventV2, - context: unknown -): ServerRequest { - const method = awsEventMethod(event); - const url = awsEventURL(event); - const headers = awsEventHeaders(event); - const body = awsEventBody(event); - - const req = new Request(url, { method, headers, body }) as ServerRequest; - - // srvx compatibility - req.runtime ??= { name: "aws-lambda" }; - // @ts-expect-error (add to srvx types) - req.runtime.aws ??= { event, context } as any; - - return new Request(url, { method, headers, body }); -} - -function awsEventMethod(event: APIGatewayProxyEvent | APIGatewayProxyEventV2): string { - return ( - (event as APIGatewayProxyEvent).httpMethod || - (event as APIGatewayProxyEventV2).requestContext?.http?.method || - "GET" - ); -} - -function awsEventURL(event: APIGatewayProxyEvent | APIGatewayProxyEventV2): URL { - const hostname = - event.headers.host || event.headers.Host || event.requestContext?.domainName || "."; - - const path = (event as APIGatewayProxyEvent).path || (event as APIGatewayProxyEventV2).rawPath; - - const query = awsEventQuery(event); - - const protocol = - (event.headers["X-Forwarded-Proto"] || event.headers["x-forwarded-proto"]) === "http" - ? "http" - : "https"; - - return new URL(`${path}${query ? `?${query}` : ""}`, `${protocol}://${hostname}`); -} - -function awsEventQuery(event: APIGatewayProxyEvent | APIGatewayProxyEventV2) { - if (typeof (event as APIGatewayProxyEventV2).rawQueryString === "string") { - return (event as APIGatewayProxyEventV2).rawQueryString; - } - const queryObj = { - ...event.queryStringParameters, - ...(event as APIGatewayProxyEvent).multiValueQueryStringParameters, - }; - return stringifyQuery(queryObj); -} - -function awsEventHeaders(event: APIGatewayProxyEvent | APIGatewayProxyEventV2): Headers { - const headers = new Headers(); - for (const [key, value] of Object.entries(event.headers)) { - if (value) { - headers.set(key, value); - } - } - if ("cookies" in event && event.cookies) { - for (const cookie of event.cookies) { - headers.append("cookie", cookie); - } - } - return headers; -} - -function awsEventBody(event: APIGatewayProxyEvent | APIGatewayProxyEventV2): BodyInit | undefined { - if (!event.body) { - return undefined; - } - if (event.isBase64Encoded) { - return Buffer.from(event.body || "", "base64"); - } - return event.body; -} - -// Outgoing (Web => AWS) - -export function awsResponseHeaders(response: Response) { - const headers: Record = Object.create(null); - for (const [key, value] of response.headers) { - if (value) { - headers[key] = Array.isArray(value) ? value.join(",") : String(value); - } - } - - const cookies = response.headers.getSetCookie(); - - return cookies.length > 0 - ? { - headers, - cookies, // ApiGateway v2 - multiValueHeaders: { "set-cookie": cookies }, // ApiGateway v1 - } - : { headers }; -} - -// AWS Lambda proxy integrations requires base64 encoded buffers -// binaryMediaTypes should be */* -// see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html -export async function awsResponseBody( - response: Response -): Promise<{ body: string; isBase64Encoded?: boolean }> { - if (!response.body) { - return { body: "" }; - } - const buffer = await toBuffer(response.body as any); - const contentType = response.headers.get("content-type") || ""; - return isTextType(contentType) - ? { body: buffer.toString("utf8") } - : { body: buffer.toString("base64"), isBase64Encoded: true }; -} - -function isTextType(contentType = "") { - return /^text\/|\/(javascript|json|xml)|utf-?8/i.test(contentType); -} - -function toBuffer(data: ReadableStream): Promise { - return new Promise((resolve, reject) => { - const chunks: Buffer[] = []; - data - .pipeTo( - new WritableStream({ - write(chunk) { - chunks.push(chunk); - }, - close() { - resolve(Buffer.concat(chunks)); - }, - abort(reason) { - reject(reason); - }, - }) - ) - .catch(reject); - }); -} diff --git a/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts b/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts index bfba0e93f4..07034b7ad1 100644 --- a/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts +++ b/src/presets/aws-lambda/runtime/aws-lambda-streaming.ts @@ -1,53 +1,9 @@ import "#nitro/virtual/polyfills"; +import { handleLambdaEventWithStream } from "srvx/aws-lambda"; import { useNitroApp } from "nitro/app"; -import { awsRequest, awsResponseHeaders } from "./_utils.ts"; - -import type { StreamingResponse } from "@netlify/functions"; -import type { Readable } from "node:stream"; -import type { APIGatewayProxyEventV2 } from "aws-lambda"; const nitroApp = useNitroApp(); -export const handler = awslambda.streamifyResponse( - async (event: APIGatewayProxyEventV2, responseStream, context) => { - const request = awsRequest(event, context); - - const response = await nitroApp.fetch(request); - - const httpResponseMetadata: Omit = { - statusCode: response.status, - ...awsResponseHeaders(response), - }; - - if (!httpResponseMetadata.headers!["transfer-encoding"]) { - httpResponseMetadata.headers!["transfer-encoding"] = "chunked"; - } - - const body = - response.body ?? - new ReadableStream({ - start(controller) { - controller.enqueue(""); - controller.close(); - }, - }); - - const writer = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata); - - const reader = body.getReader(); - await streamToNodeStream(reader, responseStream); - writer.end(); - } +export const handler = awslambda.streamifyResponse((event, responseStream, context) => + handleLambdaEventWithStream(nitroApp.fetch, event, responseStream, context) ); - -async function streamToNodeStream( - reader: Readable | ReadableStreamDefaultReader, - writer: NodeJS.WritableStream -) { - let readResult = await reader.read(); - while (!readResult.done) { - writer.write(readResult.value); - readResult = await reader.read(); - } - writer.end(); -} diff --git a/src/presets/aws-lambda/runtime/aws-lambda.ts b/src/presets/aws-lambda/runtime/aws-lambda.ts index cfe419c2ca..a8220f8caf 100644 --- a/src/presets/aws-lambda/runtime/aws-lambda.ts +++ b/src/presets/aws-lambda/runtime/aws-lambda.ts @@ -1,28 +1,12 @@ import "#nitro/virtual/polyfills"; +import { handleLambdaEvent } from "srvx/aws-lambda"; import { useNitroApp } from "nitro/app"; -import { awsRequest, awsResponseHeaders, awsResponseBody } from "./_utils.ts"; -import type { - APIGatewayProxyEvent, - APIGatewayProxyEventV2, - APIGatewayProxyResult, - APIGatewayProxyResultV2, - Context, -} from "aws-lambda"; +import type { AwsLambdaEvent } from "srvx/aws-lambda"; +import type { Context } from "aws-lambda"; const nitroApp = useNitroApp(); -export async function handler( - event: APIGatewayProxyEvent | APIGatewayProxyEventV2, - context: Context -): Promise { - const request = awsRequest(event, context); - - const response = await nitroApp.fetch(request); - - return { - statusCode: response.status, - ...awsResponseHeaders(response), - ...(await awsResponseBody(response)), - }; +export async function handler(event: AwsLambdaEvent, context: Context) { + return handleLambdaEvent(nitroApp.fetch, event, context); } diff --git a/src/presets/stormkit/runtime/stormkit.ts b/src/presets/stormkit/runtime/stormkit.ts index f4d42bea67..8c5c09dda0 100644 --- a/src/presets/stormkit/runtime/stormkit.ts +++ b/src/presets/stormkit/runtime/stormkit.ts @@ -1,6 +1,5 @@ import "#nitro/virtual/polyfills"; import { useNitroApp } from "nitro/app"; -import { awsResponseBody } from "../../aws-lambda/runtime/_utils.ts"; import type { Handler } from "aws-lambda"; import type { ServerRequest } from "srvx"; @@ -39,7 +38,7 @@ export const handler: Handler = async function const response = await nitroApp.fetch(req); - const { body, isBase64Encoded } = await awsResponseBody(response); + const { body, isBase64Encoded } = await encodeResponseBody(response); return { statusCode: response.status, @@ -53,3 +52,41 @@ function normalizeOutgoingHeaders(headers: Headers): Record { Object.entries(headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : String(v)]) ); } + +async function encodeResponseBody( + response: Response +): Promise<{ body: string; isBase64Encoded?: boolean }> { + if (!response.body) { + return { body: "" }; + } + const buffer = await toBuffer(response.body as any); + const contentType = response.headers.get("content-type") || ""; + return isTextType(contentType) + ? { body: buffer.toString("utf8") } + : { body: buffer.toString("base64"), isBase64Encoded: true }; +} + +function isTextType(contentType = "") { + return /^text\/|\/(javascript|json|xml)|utf-?8/i.test(contentType); +} + +function toBuffer(data: ReadableStream): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + data + .pipeTo( + new WritableStream({ + write(chunk) { + chunks.push(chunk); + }, + close() { + resolve(Buffer.concat(chunks)); + }, + abort(reason) { + reject(reason); + }, + }) + ) + .catch(reject); + }); +}