diff --git a/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js b/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js index a506951a5..70bdf5630 100644 --- a/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +++ b/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js @@ -5,6 +5,7 @@ import { formatToClfTime, nullIfEmpty, parseHeaders, + lowerCaseKeys, } from '../../../utils/index.js' const { isArray } = Array @@ -71,7 +72,7 @@ export default class LambdaProxyIntegrationEventV2 { const { rawHeaders } = this.#request.raw.req // NOTE FIXME request.raw.req.rawHeaders can only be null for testing (hapi shot inject()) - const headers = parseHeaders(rawHeaders || []) || {} + const headers = lowerCaseKeys(parseHeaders(rawHeaders || [])) || {} if (headers['sls-offline-authorizer-override']) { try { @@ -96,23 +97,17 @@ export default class LambdaProxyIntegrationEventV2 { } if ( - !headers['Content-Length'] && !headers['content-length'] && - !headers['Content-length'] && (typeof body === 'string' || body instanceof Buffer || body instanceof ArrayBuffer) ) { - headers['Content-Length'] = String(Buffer.byteLength(body)) + headers['content-length'] = String(Buffer.byteLength(body)) } // Set a default Content-Type if not provided. - if ( - !headers['Content-Type'] && - !headers['content-type'] && - !headers['Content-type'] - ) { - headers['Content-Type'] = 'application/json' + if (!headers['content-type']) { + headers['content-type'] = 'application/json' } } else if (typeof body === 'undefined' || body === '') { body = null diff --git a/src/utils/__tests__/lowerCaseKeys.test.js b/src/utils/__tests__/lowerCaseKeys.test.js new file mode 100644 index 000000000..682da3795 --- /dev/null +++ b/src/utils/__tests__/lowerCaseKeys.test.js @@ -0,0 +1,28 @@ +import lowerCaseKeys from '../lowerCaseKeys.js' + +describe('lowerCaseKeys', () => { + test(`should handle empty object`, () => { + const result = lowerCaseKeys({}) + expect(result).toEqual({}) + }) + + test(`should handle object with one key`, () => { + const result = lowerCaseKeys({ 'Some-Key': 'value' }) + expect(result).toEqual({ 'some-key': 'value' }) + }) + + test(`should handle object with multiple keys`, () => { + const result = lowerCaseKeys({ + 'Some-Key': 'value', + 'Another-Key': 'anotherValue', + 'lOts-OF-CAPitaLs': 'ButThisIsNotTouched', + 'already-lowercase': 'cool', + }) + expect(result).toEqual({ + 'some-key': 'value', + 'another-key': 'anotherValue', + 'lots-of-capitals': 'ButThisIsNotTouched', + 'already-lowercase': 'cool', + }) + }) +}) diff --git a/src/utils/index.js b/src/utils/index.js index bdaa721eb..c70d418a6 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -4,6 +4,7 @@ export { default as detectExecutable } from './detectExecutable.js' export { default as formatToClfTime } from './formatToClfTime.js' export { default as getHttpApiCorsConfig } from './getHttpApiCorsConfig.js' export { default as jsonPath } from './jsonPath.js' +export { default as lowerCaseKeys } from './lowerCaseKeys.js' export { default as parseHeaders } from './parseHeaders.js' export { default as parseMultiValueHeaders } from './parseMultiValueHeaders.js' export { default as parseMultiValueQueryStringParameters } from './parseMultiValueQueryStringParameters.js' diff --git a/src/utils/lowerCaseKeys.js b/src/utils/lowerCaseKeys.js new file mode 100644 index 000000000..b6480ee5a --- /dev/null +++ b/src/utils/lowerCaseKeys.js @@ -0,0 +1,6 @@ +const { entries, fromEntries } = Object + +// (obj: { [string]: string }): { [Lowercase]: string } +export default function parseHeaders(obj) { + return fromEntries(entries(obj).map(([k, v]) => [k.toLowerCase(), v])) +} diff --git a/tests/integration/httpApi-headers/handler.js b/tests/integration/httpApi-headers/handler.js new file mode 100644 index 000000000..b801a5864 --- /dev/null +++ b/tests/integration/httpApi-headers/handler.js @@ -0,0 +1,10 @@ +'use strict' + +exports.echoHeaders = async function get(event) { + return { + body: JSON.stringify({ + headersReceived: event.headers, + }), + statusCode: 200, + } +} diff --git a/tests/integration/httpApi-headers/httpApi-headers.test.js b/tests/integration/httpApi-headers/httpApi-headers.test.js new file mode 100644 index 000000000..ac787ca91 --- /dev/null +++ b/tests/integration/httpApi-headers/httpApi-headers.test.js @@ -0,0 +1,38 @@ +import { resolve } from 'path' +import fetch from 'node-fetch' +import { joinUrl, setup, teardown } from '../_testHelpers/index.js' + +jest.setTimeout(30000) + +describe('HttpApi Headers Tests', () => { + // init + beforeAll(() => + setup({ + servicePath: resolve(__dirname), + }), + ) + + // cleanup + afterAll(() => teardown()) + + test.each(['GET', 'POST'])('%s headers', async (method) => { + const url = joinUrl(TEST_BASE_URL, '/echo-headers') + const options = { + method, + headers: { + Origin: 'http://www.example.com', + 'X-Webhook-Signature': 'ABCDEF', + }, + } + + const response = await fetch(url, options) + expect(response.status).toEqual(200) + + const body = await response.json() + + expect(body.headersReceived).toMatchObject({ + origin: 'http://www.example.com', + 'x-webhook-signature': 'ABCDEF', + }) + }) +}) diff --git a/tests/integration/httpApi-headers/serverless.yml b/tests/integration/httpApi-headers/serverless.yml new file mode 100644 index 000000000..23fd19afe --- /dev/null +++ b/tests/integration/httpApi-headers/serverless.yml @@ -0,0 +1,25 @@ +service: httpapi-headers + +plugins: + - ../../../ + +provider: + memorySize: 128 + name: aws + region: us-east-1 # default + runtime: nodejs12.x + stage: dev + versionFunctions: false + httpApi: + payload: '2.0' + +functions: + echoHeaders: + events: + - httpApi: + method: get + path: echo-headers + - httpApi: + method: post + path: echo-headers + handler: handler.echoHeaders