diff --git a/lib/serverless/api-gateway.js b/lib/serverless/api-gateway.js index dcc5b40c47..10960a021c 100644 --- a/lib/serverless/api-gateway.js +++ b/lib/serverless/api-gateway.js @@ -113,72 +113,79 @@ function isLambdaProxyEvent(event) { return isGatewayV1Event(event) || isGatewayV2Event(event) || isAlbEvent(event) } +/** + * Iterates over the minimum signature properties of an event triggered by API Gateway + * to determine the version. V1 and V2 have a lot of overlap, but some signature differences. + * + * @param {object} targetEvent The event to inspect. + * @param {Array} searchFor An array of keys unique to this proxy type + * @returns {boolean} Whether this event has matches for the keys we're checking + */ +function findProperties(targetEvent, searchFor) { + const keys = Object.keys(targetEvent) + for (const el of searchFor) { + if (keys.indexOf(el) > -1) { + return true + } + } + return false +} + +/** + * We need to determine whether this invocation event was triggered from a proxy: API Gateway V1, V2, ALB, or some other service. + * API Gateway V1/V2 and ALB events aren't guaranteed always to have the same properties in all cases. There are, however, properties + * that are vary consistently depending on the service triggering the event. + * + * API Gateway v2 HTTP + * API Gateway v1 HTTP: top-level httpMethod, resource, multiValueHeaders (possibly), multiValueQueryStringParameters (possibly) + * API Gateway v1 REST same as HTTP, but without version + * ALB: very similar to API Gateway v1, but requestContext contains an elb property + */ + // See https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format -const restApiV1Keys = [ - 'body', - 'headers', +const uniqueHttpApiV1Keys = [ 'httpMethod', - 'isBase64Encoded', - 'multiValueHeaders', - 'multiValueQueryStringParameters', 'path', - 'pathParameters', - 'queryStringParameters', - 'requestContext', 'resource', - 'stageVariables' -].join(',') + 'multiValueHeaders', + 'multiValueQueryStringParameters' +] // See https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html -const httpApiV1Keys = [...restApiV1Keys.split(','), 'version'].join(',') function isGatewayV1Event(event) { - const keys = Object.keys(event).sort().join(',') - if (keys === httpApiV1Keys && event?.version === '1.0') { + if (event?.requestContext?.elb !== undefined) { + return false + } + const hasKeys = findProperties(event, uniqueHttpApiV1Keys) + if (hasKeys && event?.version === '1.0') { return true } - - return keys === restApiV1Keys + // Rest API doesn't have version, but we can check on the key matching: + return hasKeys } // See https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html -const httpApiV2Keys = [ - 'body', - 'cookies', - 'headers', - 'isBase64Encoded', - 'pathParameters', - 'queryStringParameters', - 'rawPath', - 'rawQueryString', - 'requestContext', - 'routeKey', - 'stageVariables', - 'version' -].join(',') +const uniqueHttpApiV2Keys = ['rawPath', 'rawQueryString', 'routeKey', 'cookies'] function isGatewayV2Event(event) { - const keys = Object.keys(event).sort().join(',') - return keys === httpApiV2Keys && event?.version === '2.0' + if (event?.requestContext?.elb !== undefined) { + return false + } + return findProperties(event, uniqueHttpApiV2Keys) && event?.version === '2.0' } -const albKeys = [ - 'requestContext', - 'httpMethod', - 'path', - 'queryStringParameters', - 'headers', - 'body', - 'isBase64Encoded', - 'rawHeaders', - 'multiValueQueryStringParameters', - 'pathParameters' -] - .sort() - .join(',') + +/** + * ALB can act as a proxy for Lambda. Properties have commonalities with API GateWay v1, though ALB-triggered events + * consistently carry an ARN at requestContext.elb. See + * https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#receive-event-from-load-balancer + * and https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html + * + * If we check for that property, we can accept variation in other properties. + */ function isAlbEvent(event) { - const keys = Object.keys(event).sort().join(',') - return keys === albKeys && event?.requestContext?.elb !== undefined + return event?.requestContext?.elb !== undefined } /** diff --git a/test/unit/serverless/fixtures.js b/test/unit/serverless/fixtures.js index 4200177c9c..059c40e13b 100644 --- a/test/unit/serverless/fixtures.js +++ b/test/unit/serverless/fixtures.js @@ -215,6 +215,56 @@ const httpApiGatewayV2Event = { } } +const secondApiGatewayV2Event = { + version: '2.0', + routeKey: 'ANY /', + rawPath: '/dev/', + rawQueryString: '', + headers: { + 'accept': + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', + 'accept-encoding': 'gzip, deflate, br, zstd', + 'accept-language': 'en-US,en;q=0.9', + 'content-length': '0', + 'host': 'zzz1234567890.execute-api.us-east-2.amazonaws.com', + 'priority': 'u=0, i', + 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + 'sec-ch-ua-mobile': '?0', + 'sec-ch-ua-platform': '"macOS"', + 'sec-fetch-dest': 'document', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-site': 'cross-site', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1', + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + 'x-amzn-trace-id': 'Root=1-abcdef01-01234567890abcdef0123456', + 'x-forwarded-for': '11.11.11.148', + 'x-forwarded-port': '443', + 'x-forwarded-proto': 'https' + }, + requestContext: { + accountId: '466768951184', + apiId: 'zzz1234567890', + domainName: 'zzz1234567890.execute-api.us-east-2.amazonaws.com', + domainPrefix: 'zzz1234567890', + http: { + method: 'GET', + path: '/dev/', + protocol: 'HTTP/1.1', + sourceIp: '11.11.11.148', + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' + }, + requestId: 'ABCDEF0123456=', + routeKey: 'ANY /', + stage: 'dev', + time: '26/Nov/2024:19:14:00 +0000', + timeEpoch: 1732648440329 + }, + isBase64Encoded: false +} + const albEvent = { requestContext: { elb: { @@ -292,6 +342,7 @@ module.exports = { restApiGatewayV1Event, httpApiGatewayV1Event, httpApiGatewayV2Event, + secondApiGatewayV2Event, albEvent, lambaV1InvocationEvent } diff --git a/test/unit/serverless/utils.test.js b/test/unit/serverless/utils.test.js index a715310f8d..52b9014f49 100644 --- a/test/unit/serverless/utils.test.js +++ b/test/unit/serverless/utils.test.js @@ -8,24 +8,44 @@ const test = require('node:test') const assert = require('node:assert') -const { isGatewayV1Event, isGatewayV2Event } = require('../../../lib/serverless/api-gateway') +const { + isGatewayV1Event, + isGatewayV2Event, + isAlbEvent +} = require('../../../lib/serverless/api-gateway') + const { restApiGatewayV1Event, httpApiGatewayV1Event, httpApiGatewayV2Event, - lambaV1InvocationEvent + secondApiGatewayV2Event, + lambaV1InvocationEvent, + albEvent } = require('./fixtures') test('isGatewayV1Event', () => { assert.equal(isGatewayV1Event(restApiGatewayV1Event), true) assert.equal(isGatewayV1Event(httpApiGatewayV1Event), true) assert.equal(isGatewayV1Event(httpApiGatewayV2Event), false) + assert.equal(isGatewayV1Event(secondApiGatewayV2Event), false) assert.equal(isGatewayV1Event(lambaV1InvocationEvent), false) + assert.equal(isGatewayV1Event(albEvent), false) }) test('isGatewayV2Event', () => { assert.equal(isGatewayV2Event(restApiGatewayV1Event), false) assert.equal(isGatewayV2Event(httpApiGatewayV1Event), false) assert.equal(isGatewayV2Event(httpApiGatewayV2Event), true) + assert.equal(isGatewayV2Event(secondApiGatewayV2Event), true) assert.equal(isGatewayV2Event(lambaV1InvocationEvent), false) + assert.equal(isGatewayV2Event(albEvent), false) +}) + +test('isAlbEvent', () => { + assert.equal(isAlbEvent(restApiGatewayV1Event), false) + assert.equal(isAlbEvent(httpApiGatewayV1Event), false) + assert.equal(isAlbEvent(httpApiGatewayV2Event), false) + assert.equal(isAlbEvent(secondApiGatewayV2Event), false) + assert.equal(isAlbEvent(lambaV1InvocationEvent), false) + assert.equal(isAlbEvent(albEvent), true) })