diff --git a/src/events/http/Endpoint.js b/src/events/http/Endpoint.js index 3b82bb576..be9d82c32 100644 --- a/src/events/http/Endpoint.js +++ b/src/events/http/Endpoint.js @@ -92,7 +92,7 @@ export default class Endpoint { // loosely based on: // https://github.com/serverless/serverless/blob/v1.59.2/lib/plugins/aws/package/compile/events/apiGateway/lib/validate.js#L380 #getIntegration(http) { - const { integration, async: isAsync } = http + const { integration } = http if (integration) { const normalizedIntegration = integration.toUpperCase().replace('-', '_') if (normalizedIntegration === 'LAMBDA') { @@ -104,10 +104,6 @@ export default class Endpoint { return normalizedIntegration } - if (isAsync) { - return 'AWS' - } - return 'AWS_PROXY' } diff --git a/src/events/http/HttpServer.js b/src/events/http/HttpServer.js index 3f69628a1..b62240702 100644 --- a/src/events/http/HttpServer.js +++ b/src/events/http/HttpServer.js @@ -493,7 +493,7 @@ export default class HttpServer { const response = h.response() const contentType = request.mime || 'application/json' // default content type - const { integration, requestTemplates } = endpoint + const { integration, requestTemplates, async: isAsync } = endpoint // default request template to '' if we don't have a definition pushed in from serverless or endpoint const requestTemplate = @@ -576,6 +576,7 @@ export default class HttpServer { request, stage, endpoint.routeKey, + isAsync, additionalRequestContext, ) : new LambdaProxyIntegrationEvent( @@ -583,6 +584,7 @@ export default class HttpServer { stage, requestPath, endpoint.isHttpApi ? endpoint.routeKey : null, + isAsync, additionalRequestContext, ) @@ -599,7 +601,16 @@ export default class HttpServer { let err try { - result = await lambdaFunction.runHandler() + if (isAsync) { + // API Gateway returns 200 automatically + lambdaFunction.runHandler().catch(() => {}) + result = { + body: '', + statusCode: 200, + } + } else { + result = await lambdaFunction.runHandler() + } } catch (_err) { err = _err } diff --git a/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js b/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js index 838790a4b..340a7b276 100644 --- a/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js +++ b/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js @@ -30,12 +30,22 @@ export default class LambdaProxyIntegrationEvent { #stage = null - constructor(request, stage, path, routeKey, additionalRequestContext) { + #isAsync = false + + constructor( + request, + stage, + path, + routeKey, + isAsync, + additionalRequestContext, + ) { this.#additionalRequestContext = additionalRequestContext || {} this.#path = path this.#routeKey = routeKey this.#request = request this.#stage = stage + this.#isAsync = isAsync || false } create() { @@ -96,6 +106,16 @@ export default class LambdaProxyIntegrationEvent { headers['Content-Length'] = String(byteLength(body)) } + if (this.#isAsync) { + if (headers['Content-Type'] === 'application/x-www-form-urlencoded') { + body = Object.fromEntries(new URLSearchParams(body).entries()) + } + + if (headers['Content-Type'] === 'application/json') { + body = JSON.parse(body) + } + } + // Set a default Content-Type if not provided. if ( !headers['Content-Type'] && diff --git a/tests/old-unit/LambdaProxyIntegrationEvent.test.js b/tests/old-unit/LambdaProxyIntegrationEvent.test.js index 143ee5d71..3a03d7bb6 100644 --- a/tests/old-unit/LambdaProxyIntegrationEvent.test.js +++ b/tests/old-unit/LambdaProxyIntegrationEvent.test.js @@ -747,6 +747,58 @@ describe('LambdaProxyIntegrationEvent', () => { }) }) + describe('with isAsync', () => { + it('should parse the body of a call with a application/json header', () => { + const requestBuilder = new RequestBuilder('POST', '/async') + requestBuilder.addBody(JSON.stringify({ key: 'value' })) + requestBuilder.addHeader('Content-Type', 'application/json') + const request = requestBuilder.toObject() + const path = 'path' + const stageVariables = 'stageVariables' + const routeKey = undefined + const isAsync = true + + const lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEvent( + request, + stage, + path, + stageVariables, + routeKey, + isAsync, + ).create() + + assert.equal( + lambdaProxyIntegrationEvent.body, + JSON.stringify({ key: 'value' }), + ) + }) + + it('should parse the body of a call with a application/x-www-form-urlencoded header', () => { + const requestBuilder = new RequestBuilder('POST', '/async') + requestBuilder.addBody(new URLSearchParams({ key: 'value' }).toString()) + requestBuilder.addHeader( + 'Content-Type', + 'application/x-www-form-urlencoded', + ) + const request = requestBuilder.toObject() + const path = 'path' + const stageVariables = 'stageVariables' + const routeKey = undefined + const isAsync = true + + const lambdaProxyIntegrationEvent = new LambdaProxyIntegrationEvent( + request, + stage, + path, + stageVariables, + routeKey, + isAsync, + ).create() + + assert.equal(lambdaProxyIntegrationEvent.body, 'key=value') + }) + }) + describe('with operation name', () => { const requestBuilder = new RequestBuilder('GET', '/fn1') const request = requestBuilder.toObject() diff --git a/tests/old-unit/offline.test.js b/tests/old-unit/offline.test.js index 5b50eb484..b55d78205 100644 --- a/tests/old-unit/offline.test.js +++ b/tests/old-unit/offline.test.js @@ -388,6 +388,34 @@ describe('Offline', () => { }) }) + describe('lambda-proxy integration with async', () => { + it('should return 200 even if lambda fails', async () => { + const offline = await new OfflineBuilder() + .addFunctionConfig('async', { + events: [ + { + http: { + async: true, + method: 'GET', + path: 'async', + }, + }, + ], + handler: 'tests/old-unit/fixtures/handler.throwDone', + }) + .toObject() + + const res = await offline.inject({ + method: 'GET', + payload: { data: 'input' }, + url: '/dev/async', + }) + + assert.equal(res.headers['content-type'], 'application/json') + assert.equal(res.statusCode, 200) + }) + }) + describe('with catch-all route', () => { it('should match arbitary route', async () => { const offline = new OfflineBuilder().addFunctionConfig('test', {