From 5adbe5c16cb64bfd858a619d22b064795b13564f Mon Sep 17 00:00:00 2001 From: jackbaker Date: Thu, 10 Jul 2025 13:41:28 +0100 Subject: [PATCH 1/3] fix: stringify response headers as ALB expects response headers to be type string This was identified through using express within AWS Lambda which returns content-length header as a number which causes ALB to throw a 502 bad gateway exception. This change fixes this issue. --- src/event-sources/aws/alb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/event-sources/aws/alb.js b/src/event-sources/aws/alb.js index 569ba742..05c2109d 100644 --- a/src/event-sources/aws/alb.js +++ b/src/event-sources/aws/alb.js @@ -52,7 +52,7 @@ const getResponseToAlb = ({ const multiValueHeaders = !event.headers ? getMultiValueHeaders({ headers: responseHeaders }) : undefined const headers = event.headers ? Object.entries(responseHeaders).reduce((acc, [k, v]) => { - acc[k] = Array.isArray(v) ? v[0] : v + acc[k] = Array.isArray(v) ? String(v[0]) : String(v) return acc }, {}) : undefined From c52ce29065c2ee0b5d1e3d0e530051079524aece Mon Sep 17 00:00:00 2001 From: jackbaker Date: Thu, 10 Jul 2025 14:10:14 +0100 Subject: [PATCH 2/3] fix: stringify response headers as ALB expects response headers to be type string This was identified through using express within AWS Lambda which returns content-length header as a number which causes ALB to throw a 502 bad gateway exception. This change fixes this issue. --- __tests__/integration-alb.js | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 __tests__/integration-alb.js diff --git a/__tests__/integration-alb.js b/__tests__/integration-alb.js new file mode 100644 index 00000000..836a0fad --- /dev/null +++ b/__tests__/integration-alb.js @@ -0,0 +1,37 @@ +const express = require('express') +const serverlessExpress = require('../src/index') +const { + makeEvent, + makeResponse +} = require('../jest-helpers') + +describe('alb:express integration tests', () => { + test('reasponse headers are of type string', async () => { + const app = express() + const router = express.Router() + app.use('/', router) + const serverlessExpressInstance = serverlessExpress({ app }) + router.get('/foo', (req, res) => { + res.send('123') + }) + const event = makeEvent({ + eventSourceName: 'alb', + path: '/foo', + httpMethod: 'GET', + headers: {} + }) + const response = await serverlessExpressInstance(event) + const expectedResponse = makeResponse({ + eventSourceName: 'alb', + body: '123', + headers: { + 'content-length': '3', + 'content-type': 'text/html; charset=utf-8', + 'x-powered-by': 'Express', + etag: 'W/"3-QL0AFWMIX8NRZTKeof9cXsvbvu8"' + }, + multiValueHeaders: undefined + }) + expect(response).toMatchObject(expectedResponse) + }) +}) From 6ea0b024e979680b19ee1079dbe2efa69f2bc855 Mon Sep 17 00:00:00 2001 From: jackbaker Date: Wed, 3 Dec 2025 11:24:22 +0000 Subject: [PATCH 3/3] fix: fixing issue with async error handling within the SERVERLESS_EXPRESS:PROXY This change handles non-async and async errors thrown within `forwardRequestToNodeServer` and adds an integration test (this was previously skipped). --- __tests__/integration-alb.js | 6 +-- __tests__/integration.js | 77 ++++++++++++++++++++++++++++-------- jest-helpers/index.js | 36 ++++++++--------- package-lock.json | 2 +- src/configure.js | 22 ++++++----- src/transport.js | 4 +- 6 files changed, 99 insertions(+), 48 deletions(-) diff --git a/__tests__/integration-alb.js b/__tests__/integration-alb.js index 836a0fad..5f1447c9 100644 --- a/__tests__/integration-alb.js +++ b/__tests__/integration-alb.js @@ -6,7 +6,7 @@ const { } = require('../jest-helpers') describe('alb:express integration tests', () => { - test('reasponse headers are of type string', async () => { + test('response headers are of type string', async () => { const app = express() const router = express.Router() app.use('/', router) @@ -15,14 +15,14 @@ describe('alb:express integration tests', () => { res.send('123') }) const event = makeEvent({ - eventSourceName: 'alb', + eventSourceName: 'AWS_ALB', path: '/foo', httpMethod: 'GET', headers: {} }) const response = await serverlessExpressInstance(event) const expectedResponse = makeResponse({ - eventSourceName: 'alb', + eventSourceName: 'AWS_ALB', body: '123', headers: { 'content-length': '3', diff --git a/__tests__/integration.js b/__tests__/integration.js index 2e6ee91d..1f853e24 100644 --- a/__tests__/integration.js +++ b/__tests__/integration.js @@ -10,6 +10,7 @@ const { makeResponse, EACH_MATRIX } = require('../jest-helpers') +const { getEventSource } = require('../src/event-sources') const jestHelpersPath = path.join(__dirname, '..', 'jest-helpers') let app, router, serverlessExpressInstance @@ -115,7 +116,7 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo res.json({ xHeaders }) }) const event = makeEvent({ - eventSourceName: 'apiGatewayV1', + eventSourceName: 'AWS_API_GATEWAY_V1', path: '/foo', httpMethod: 'GET', multiValueHeaders: undefined, @@ -126,7 +127,7 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo }) const response = await serverlessExpressInstance(event) const expectedResponse = makeResponse({ - eventSourceName: 'apiGatewayV1', + eventSourceName: 'AWS_API_GATEWAY_V1', body: JSON.stringify({ xHeaders: { 'x-header-one': 'Value1', @@ -265,8 +266,8 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo const etagRegex = /^W\/.*$/ const lastModifiedRegex = /^.* GMT$/ switch (eventSourceName) { - case 'alb': - case 'apiGatewayV1': + case 'AWS_ALB': + case 'AWS_API_GATEWAY_V1': expect(response.multiValueHeaders.etag.length).toEqual(1) expect(response.multiValueHeaders.etag[0]).toMatch(etagRegex) expect(response.multiValueHeaders['last-modified'].length).toEqual(1) @@ -274,8 +275,8 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo delete response.multiValueHeaders.etag delete response.multiValueHeaders['last-modified'] break - case 'azureHttpFunctionV4': - case 'azureHttpFunctionV3': + case 'AZURE_HTTP_FUNCTION_V4': + case 'AZURE_HTTP_FUNCTION_V3': expectedResponse.body = Buffer.from(samLogoBase64, 'base64') expectedResponse.isBase64Encoded = false expect(response.headers.etag).toMatch(etagRegex) @@ -283,13 +284,13 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo delete response.headers.etag delete response.headers['last-modified'] break - case 'apiGatewayV2': + case 'AWS_API_GATEWAY_V2': expect(response.headers.etag).toMatch(etagRegex) expect(response.headers['last-modified']).toMatch(lastModifiedRegex) delete response.headers.etag delete response.headers['last-modified'] break - case 'lambdaEdge': + case 'AWS_LAMBDA_EDGE': expect(response.headers.etag.length).toEqual(1) expect(response.headers.etag[0].key).toMatch('etag') expect(response.headers.etag[0].value).toMatch(etagRegex) @@ -420,12 +421,56 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo expect(response).toEqual(expectedResponse) }) - test.skip('respondToEventSourceWithError', async () => { - const response = await serverlessExpressInstance(null) - expect(response).toEqual({ - statusCode: 500, - body: '', - multiValueHeaders: {} + describe('respondToEventSourceWithError', () => { + let event + let eventSource + let getRequestSpy + + beforeEach(() => { + event = makeEvent({ + eventSourceName, + path: '/users', + httpMethod: 'GET' + }) + eventSource = getEventSource({ eventSourceName }) + getRequestSpy = jest.spyOn(eventSource, 'getRequest').mockImplementation(() => { + throw new URIError('URI malformed') + }) + }) + + afterEach(() => { + getRequestSpy.mockRestore() + }) + + test('respondWithErrors: true', async () => { + serverlessExpressInstance = serverlessExpress({ + app, + eventSource, + respondWithErrors: true + }) + + const response = await serverlessExpressInstance(event) + const statusCode = response.statusCode || response.status + + expect(statusCode).toBe(500) + expect(response.body).toContain('URIError') + expect(response.body).toContain('URI malformed') + expect(getRequestSpy).toHaveBeenCalledWith(expect.objectContaining({ event })) + }) + + test('respondWithErrors: false', async () => { + serverlessExpressInstance = serverlessExpress({ + app, + eventSource, + respondWithErrors: false + }) + + const response = await serverlessExpressInstance(event) + const statusCode = response.statusCode || response.status + + expect(statusCode).toBe(500) + expect(response.body).toEqual('') + expect(getRequestSpy).toHaveBeenCalledWith(expect.objectContaining({ event })) }) }) @@ -471,8 +516,8 @@ describe.each(EACH_MATRIX)('%s:%s: integration tests', (eventSourceName, framewo jest.useRealTimers() switch (eventSourceName) { - case 'azureHttpFunctionV4': - case 'azureHttpFunctionV3': + case 'AZURE_HTTP_FUNCTION_V4': + case 'AZURE_HTTP_FUNCTION_V3': expectedResponse.cookies = [ { domain: 'mafoo.com', diff --git a/jest-helpers/index.js b/jest-helpers/index.js index f826cb25..4145c81d 100644 --- a/jest-helpers/index.js +++ b/jest-helpers/index.js @@ -6,12 +6,12 @@ const { makeAzureHttpFunctionV3Event, makeAzureHttpFunctionV3Response } = requir const { makeAzureHttpFunctionV4Event, makeAzureHttpFunctionV4Response } = require('./azure-http-function-v4-event') const EVENT_SOURCE_NAMES = [ - 'alb', - 'apiGatewayV1', - 'apiGatewayV2', - 'lambdaEdge', - 'azureHttpFunctionV3', - 'azureHttpFunctionV4' + 'AWS_ALB', + 'AWS_API_GATEWAY_V1', + 'AWS_API_GATEWAY_V2', + 'AWS_LAMBDA_EDGE', + 'AZURE_HTTP_FUNCTION_V3', + 'AZURE_HTTP_FUNCTION_V4' ] const FRAMEWORK_NAMES = [ @@ -49,17 +49,17 @@ class MockContext { function makeEvent ({ eventSourceName, ...rest }) { switch (eventSourceName) { - case 'alb': + case 'AWS_ALB': return makeAlbEvent(rest) - case 'apiGatewayV1': + case 'AWS_API_GATEWAY_V1': return makeApiGatewayV1Event(rest) - case 'apiGatewayV2': + case 'AWS_API_GATEWAY_V2': return makeApiGatewayV2Event(rest) - case 'lambdaEdge': + case 'AWS_LAMBDA_EDGE': return makeLambdaEdgeEvent(rest) - case 'azureHttpFunctionV3': + case 'AZURE_HTTP_FUNCTION_V3': return makeAzureHttpFunctionV3Event(rest) - case 'azureHttpFunctionV4': + case 'AZURE_HTTP_FUNCTION_V4': return makeAzureHttpFunctionV4Event(rest) default: throw new Error(`Unknown eventSourceName ${eventSourceName}`) @@ -68,17 +68,17 @@ function makeEvent ({ eventSourceName, ...rest }) { function makeResponse ({ eventSourceName, ...rest }, { shouldConvertContentLengthToInt = false } = {}) { switch (eventSourceName) { - case 'alb': + case 'AWS_ALB': return makeAlbResponse(rest) - case 'apiGatewayV1': + case 'AWS_API_GATEWAY_V1': return makeApiGatewayV1Response(rest) - case 'apiGatewayV2': + case 'AWS_API_GATEWAY_V2': return makeApiGatewayV2Response(rest, { shouldConvertContentLengthToInt }) - case 'lambdaEdge': + case 'AWS_LAMBDA_EDGE': return makeLambdaEdgeResponse(rest) - case 'azureHttpFunctionV3': + case 'AZURE_HTTP_FUNCTION_V3': return makeAzureHttpFunctionV3Response(rest, { shouldConvertContentLengthToInt }) - case 'azureHttpFunctionV4': + case 'AZURE_HTTP_FUNCTION_V4': return makeAzureHttpFunctionV4Response(rest, { shouldConvertContentLengthToInt }) default: throw new Error(`Unknown eventSourceName ${eventSourceName}`) diff --git a/package-lock.json b/package-lock.json index 705bfc8e..8ccb88d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "typescript": "^5.3.3" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/src/configure.js b/src/configure.js index 02dbaa3a..4d6029f3 100644 --- a/src/configure.js +++ b/src/configure.js @@ -68,6 +68,17 @@ function configure ({ promise, resolutionMode }) + const handleError = (error) => { + respondToEventSourceWithError({ + error, + resolver, + log, + respondWithErrors, + eventSourceName, + eventSource, + event + }) + } try { forwardRequestToNodeServer({ @@ -81,16 +92,9 @@ function configure ({ eventSource, eventSourceRoutes, log - }) + }).catch(handleError) } catch (error) { - respondToEventSourceWithError({ - error, - resolver, - log, - respondWithErrors, - eventSourceName, - eventSource - }) + handleError(error) } }) } diff --git a/src/transport.js b/src/transport.js index e1993277..bc5243b8 100644 --- a/src/transport.js +++ b/src/transport.js @@ -55,7 +55,8 @@ function respondToEventSourceWithError ({ log, respondWithErrors, eventSourceName, - eventSource + eventSource, + event }) { log.error('SERVERLESS_EXPRESS:RESPOND_TO_EVENT_SOURCE_WITH_ERROR', error) @@ -73,6 +74,7 @@ function respondToEventSourceWithError ({ const body = respondWithErrors ? error.stack : '' const errorResponse = eventSource.getResponse({ + event, statusCode: 500, body, headers: {},