diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index e59175770..724b676ab 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -439,6 +439,7 @@ export default class HttpServer { // payload processing const encoding = detectEncoding(request) + request.raw.req.payload = request.payload request.payload = request.payload && request.payload.toString(encoding) request.rawPayload = request.payload diff --git a/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js b/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js index 9f624bd70..30b92f124 100644 --- a/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js +++ b/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js @@ -4,6 +4,7 @@ import { env } from "node:process" import { log } from "@serverless/utils/log.js" import { decodeJwt } from "jose" import { + detectEncoding, formatToClfTime, nullIfEmpty, parseHeaders, @@ -63,6 +64,7 @@ export default class LambdaProxyIntegrationEvent { } let body = this.#request.payload + let isBase64Encoded = false const { rawHeaders, url } = this.#request.raw.req @@ -80,6 +82,15 @@ export default class LambdaProxyIntegrationEvent { } if (body) { + if ( + this.#request.raw.req.payload && + detectEncoding(this.#request) === "binary" + ) { + body = Buffer.from(this.#request.raw.req.payload).toString("base64") + headers["Content-Length"] = String(Buffer.byteLength(body, "base64")) + isBase64Encoded = true + } + if (typeof body !== "string") { // this.#request.payload is NOT the same as the rawPayload body = this.#request.rawPayload @@ -155,7 +166,7 @@ export default class LambdaProxyIntegrationEvent { body, headers, httpMethod, - isBase64Encoded: false, // TODO hook up + isBase64Encoded, multiValueHeaders: parseMultiValueHeaders( // NOTE FIXME request.raw.req.rawHeaders can only be null for testing (hapi shot inject()) rawHeaders || [], diff --git a/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js b/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js index b072bf1e0..72551e7de 100644 --- a/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +++ b/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js @@ -3,6 +3,7 @@ import { env } from "node:process" import { log } from "@serverless/utils/log.js" import { decodeJwt } from "jose" import { + detectEncoding, formatToClfTime, lowerCaseKeys, nullIfEmpty, @@ -55,6 +56,7 @@ export default class LambdaProxyIntegrationEventV2 { } let body = this.#request.payload + let isBase64Encoded = false const { rawHeaders } = this.#request.raw.req @@ -72,6 +74,15 @@ export default class LambdaProxyIntegrationEventV2 { } if (body) { + if ( + this.#request.raw.req.payload && + detectEncoding(this.#request) === "binary" + ) { + body = Buffer.from(this.#request.raw.req.payload).toString("base64") + headers["content-length"] = String(Buffer.byteLength(body, "base64")) + isBase64Encoded = true + } + if (typeof body !== "string") { // this.#request.payload is NOT the same as the rawPayload body = this.#request.rawPayload @@ -145,7 +156,7 @@ export default class LambdaProxyIntegrationEventV2 { body, cookies, headers, - isBase64Encoded: false, + isBase64Encoded, pathParameters: nullIfEmpty(pathParams), queryStringParameters: this.#request.url.search ? parseQueryStringParametersForPayloadV2(this.#request.url.searchParams) diff --git a/tests/integration/httpApi-headers/httpApi-headers.test.js b/tests/integration/httpApi-headers/httpApi-headers.test.js index 1ca82ed8f..f6db7de2b 100644 --- a/tests/integration/httpApi-headers/httpApi-headers.test.js +++ b/tests/integration/httpApi-headers/httpApi-headers.test.js @@ -1,4 +1,5 @@ import assert from "node:assert" +import { Buffer } from "node:buffer" import { join } from "desm" import { setup, teardown } from "../../_testHelpers/index.js" import { BASE_URL } from "../../config.js" @@ -13,15 +14,60 @@ describe("HttpApi Headers Tests", function desc() { afterEach(() => teardown()) // - ;["GET", "POST"].forEach((method) => { - it(`${method} headers`, async () => { - const url = new URL("/echo-headers", BASE_URL) + ;[ + { + desiredContentLengthHeader: "Content-Length", + desiredOriginHeader: "Origin", + desiredWebhookSignatureHeder: "X-Webhook-Signature", + payloadVersion: "1.0", + }, + { + desiredContentLengthHeader: "content-length", + desiredOriginHeader: "origin", + desiredWebhookSignatureHeder: "x-webhook-signature", + payloadVersion: "2.0", + }, + ].forEach((t) => { + ;["GET", "POST"].forEach((method) => { + it(`${method} headers (payload ${t.payloadVersion})`, async () => { + const url = new URL(`/echo-headers-${t.payloadVersion}`, BASE_URL) + const options = { + headers: { + Origin: "http://www.example.com", + "X-Webhook-Signature": "ABCDEF", + }, + method, + } + + const response = await fetch(url, options) + + assert.equal(response.status, 200) + + const body = await response.json() + + assert.equal( + body.headersReceived[t.desiredOriginHeader], + "http://www.example.com", + ) + assert.equal( + body.headersReceived[t.desiredWebhookSignatureHeder], + "ABCDEF", + ) + assert.equal(body.isBase64EncodedReceived, false) + }) + }) + + it(`multipart/form-data headers are base64 encoded (payload ${t.payloadVersion})`, async () => { + const url = new URL(`/echo-headers-${t.payloadVersion}`, BASE_URL) const options = { + body: `------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="file"; filename="file.txt"\r\nContent-Type: text/plain\r\n\r\n\u0001content\u0003\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n`, headers: { + "Content-Type": + "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW", Origin: "http://www.example.com", "X-Webhook-Signature": "ABCDEF", }, - method, + method: "POST", } const response = await fetch(url, options) @@ -30,8 +76,23 @@ describe("HttpApi Headers Tests", function desc() { const body = await response.json() - assert.equal(body.headersReceived.origin, "http://www.example.com") - assert.equal(body.headersReceived["x-webhook-signature"], "ABCDEF") + assert.equal( + body.headersReceived[t.desiredOriginHeader], + "http://www.example.com", + ) + assert.equal( + body.headersReceived[t.desiredWebhookSignatureHeder], + "ABCDEF", + ) + assert.equal( + Number.parseInt(body.headersReceived[t.desiredContentLengthHeader], 10), + options.body.length, + ) + assert.equal(body.isBase64EncodedReceived, true) + assert.equal( + body.bodyReceived, + Buffer.from(options.body).toString("base64"), + ) }) }) }) diff --git a/tests/integration/httpApi-headers/serverless.yml b/tests/integration/httpApi-headers/serverless.yml index df7b20010..a142fb73a 100644 --- a/tests/integration/httpApi-headers/serverless.yml +++ b/tests/integration/httpApi-headers/serverless.yml @@ -9,8 +9,6 @@ plugins: provider: architecture: arm64 deploymentMethod: direct - httpApi: - payload: "2.0" memorySize: 1024 name: aws region: us-east-1 @@ -19,12 +17,25 @@ provider: versionFunctions: false functions: + echoHeadersV1: + httpApi: + payload: "1.0" + events: + - httpApi: + method: get + path: /echo-headers-1.0 + - httpApi: + method: post + path: /echo-headers-1.0 + handler: src/handler.echoHeaders echoHeaders: + httpApi: + payload: "2.0" events: - httpApi: method: get - path: /echo-headers + path: /echo-headers-2.0 - httpApi: method: post - path: /echo-headers + path: /echo-headers-2.0 handler: src/handler.echoHeaders diff --git a/tests/integration/httpApi-headers/src/handler.js b/tests/integration/httpApi-headers/src/handler.js index a4ccc8631..ef4c44900 100644 --- a/tests/integration/httpApi-headers/src/handler.js +++ b/tests/integration/httpApi-headers/src/handler.js @@ -3,7 +3,9 @@ const { stringify } = JSON export async function echoHeaders(event) { return { body: stringify({ + bodyReceived: event.body, headersReceived: event.headers, + isBase64EncodedReceived: event.isBase64Encoded, }), statusCode: 200, }