Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Improved AWS Lambda event detection #2496

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 38 additions & 29 deletions lib/serverless/api-gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,38 +106,45 @@ function isLambdaProxyEvent(event) {
return isGatewayV1Event(event) || isGatewayV2Event(event)
}

function isGatewayV1Event(event) {
let result = false

if (event?.version === '1.0') {
result = true
} else if (
typeof event?.path === 'string' &&
(event.headers ?? event.multiValueHeaders) &&
typeof event?.httpMethod === 'string'
// eslint-disable-next-line sonarjs/no-duplicated-branches
) {
result = true
}
const v1Keys = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find any information on what AWS does when some field from this list is not included in the payload. Does it send with field: null? Or does it completely omit the field. We are assuming that it sends field: null with this implementation, because the v1 example shows some such fields.

'body',
'headers',
'httpMethod',
'isBase64Encoded',
'multiValueHeaders',
'multiValueQueryStringParameters',
'path',
'pathParameters',
'queryStringParameters',
'requestContext',
'resource',
'stageVariables',
'version'
].join(',')

return result
function isGatewayV1Event(event) {
const keys = Object.keys(event).sort().join(',')
return keys === v1Keys && event?.version === '1.0'
}

function isGatewayV2Event(event) {
let result = false

if (event?.version === '2.0') {
result = true
} else if (
typeof event?.requestContext?.http?.path === 'string' &&
Object.prototype.toString.call(event.headers) === '[object Object]' &&
typeof event?.requestContext?.http?.method === 'string'
// eslint-disable-next-line sonarjs/no-duplicated-branches
) {
result = true
}
const v2Keys = [
'body',
'cookies',
'headers',
'isBase64Encoded',
'pathParameters',
'queryStringParameters',
'rawPath',
'rawQueryString',
'requestContext',
'routeKey',
'stageVariables',
'version'
].join(',')

return result
function isGatewayV2Event(event) {
const keys = Object.keys(event).sort().join(',')
return keys === v2Keys && event?.version === '2.0'
}

/**
Expand All @@ -155,5 +162,7 @@ module.exports = {
LambdaProxyWebRequest,
LambdaProxyWebResponse,
isLambdaProxyEvent,
isValidLambdaProxyResponse
isValidLambdaProxyResponse,
isGatewayV1Event,
isGatewayV2Event
}
68 changes: 1 addition & 67 deletions test/unit/serverless/api-gateway-v2.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,73 +13,7 @@ const AwsLambda = require('../../../lib/serverless/aws-lambda')

const ATTR_DEST = require('../../../lib/config/attribute-filter').DESTINATIONS

// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
const v2Event = {
version: '2.0',
routeKey: '$default',
rawPath: '/my/path',
rawQueryString: 'parameter1=value1&parameter1=value2&parameter2=value',
cookies: ['cookie1', 'cookie2'],
headers: {
header1: 'value1',
header2: 'value1,value2',
accept: 'application/json'
},
queryStringParameters: {
parameter1: 'value1,value2',
parameter2: 'value',
name: 'me',
team: 'node agent'
},
requestContext: {
accountId: '123456789012',
apiId: 'api-id',
authentication: {
clientCert: {
clientCertPem: 'CERT_CONTENT',
subjectDN: 'www.example.com',
issuerDN: 'Example issuer',
serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1',
validity: {
notBefore: 'May 28 12:30:02 2019 GMT',
notAfter: 'Aug 5 09:36:04 2021 GMT'
}
}
},
authorizer: {
jwt: {
claims: {
claim1: 'value1',
claim2: 'value2'
},
scopes: ['scope1', 'scope2']
}
},
domainName: 'id.execute-api.us-east-1.amazonaws.com',
domainPrefix: 'id',
http: {
method: 'POST',
path: '/my/path',
protocol: 'HTTP/1.1',
sourceIp: '192.0.2.1',
userAgent: 'agent'
},
requestId: 'id',
routeKey: '$default',
stage: '$default',
time: '12/Mar/2020:19:03:58 +0000',
timeEpoch: 1583348638390
},
body: 'Hello from Lambda',
pathParameters: {
parameter1: 'value1'
},
isBase64Encoded: false,
stageVariables: {
stageVariable1: 'value1',
stageVariable2: 'value2'
}
}
const { gatewayV2Event: v2Event } = require('./fixtures')

tap.beforeEach((t) => {
// This env var suppresses console output we don't need to inspect.
Expand Down
181 changes: 181 additions & 0 deletions test/unit/serverless/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
const gatewayV1Event = {
version: '1.0',
resource: '/my/path',
path: '/my/path',
httpMethod: 'GET',
headers: {
header1: 'value1',
header2: 'value2'
},
multiValueHeaders: {
header1: ['value1'],
header2: ['value1', 'value2']
},
queryStringParameters: {
parameter1: 'value1',
parameter2: 'value'
},
multiValueQueryStringParameters: {
parameter1: ['value1', 'value2'],
parameter2: ['value']
},
requestContext: {
accountId: '123456789012',
apiId: 'id',
authorizer: {
claims: null,
scopes: null
},
domainName: 'id.execute-api.us-east-1.amazonaws.com',
domainPrefix: 'id',
extendedRequestId: 'request-id',
httpMethod: 'GET',
identity: {
accessKey: null,
accountId: null,
caller: null,
cognitoAuthenticationProvider: null,
cognitoAuthenticationType: null,
cognitoIdentityId: null,
cognitoIdentityPoolId: null,
principalOrgId: null,
sourceIp: '192.0.2.1',
user: null,
userAgent: 'user-agent',
userArn: null,
clientCert: {
clientCertPem: 'CERT_CONTENT',
subjectDN: 'www.example.com',
issuerDN: 'Example issuer',
serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1',
validity: {
notBefore: 'May 28 12:30:02 2019 GMT',
notAfter: 'Aug 5 09:36:04 2021 GMT'
}
}
},
path: '/my/path',
protocol: 'HTTP/1.1',
requestId: 'id=',
requestTime: '04/Mar/2020:19:15:17 +0000',
requestTimeEpoch: 1583349317135,
resourceId: null,
resourcePath: '/my/path',
stage: '$default'
},
pathParameters: null,
stageVariables: null,
body: 'Hello from Lambda!',
isBase64Encoded: false
}

// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
const gatewayV2Event = {
version: '2.0',
routeKey: '$default',
rawPath: '/my/path',
rawQueryString: 'parameter1=value1&parameter1=value2&parameter2=value',
cookies: ['cookie1', 'cookie2'],
headers: {
header1: 'value1',
header2: 'value1,value2',
accept: 'application/json'
},
queryStringParameters: {
parameter1: 'value1,value2',
parameter2: 'value',
name: 'me',
team: 'node agent'
},
requestContext: {
accountId: '123456789012',
apiId: 'api-id',
authentication: {
clientCert: {
clientCertPem: 'CERT_CONTENT',
subjectDN: 'www.example.com',
issuerDN: 'Example issuer',
serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1',
validity: {
notBefore: 'May 28 12:30:02 2019 GMT',
notAfter: 'Aug 5 09:36:04 2021 GMT'
}
}
},
authorizer: {
jwt: {
claims: {
claim1: 'value1',
claim2: 'value2'
},
scopes: ['scope1', 'scope2']
}
},
domainName: 'id.execute-api.us-east-1.amazonaws.com',
domainPrefix: 'id',
http: {
method: 'POST',
path: '/my/path',
protocol: 'HTTP/1.1',
sourceIp: '192.0.2.1',
userAgent: 'agent'
},
requestId: 'id',
routeKey: '$default',
stage: '$default',
time: '12/Mar/2020:19:03:58 +0000',
timeEpoch: 1583348638390
},
body: 'Hello from Lambda',
pathParameters: {
parameter1: 'value1'
},
isBase64Encoded: false,
stageVariables: {
stageVariable1: 'value1',
stageVariable2: 'value2'
}
}

// Event used when one Lambda directly invokes another Lambda.
// https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-async-destinations
const lambaV1InvocationEvent = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not completely convinced this is the only payload shape that Lambda sends with direct invocations. But I can't find any other documentation to prove that.

version: '1.0',
timestamp: '2019-11-14T18:16:05.568Z',
requestContext: {
requestId: 'e4b46cbf-b738-xmpl-8880-a18cdf61200e',
functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:$LATEST',
condition: 'RetriesExhausted',
approximateInvokeCount: 3
},
requestPayload: {
ORDER_IDS: [
'9e07af03-ce31-4ff3-xmpl-36dce652cb4f',
'637de236-e7b2-464e-xmpl-baf57f86bb53',
'a81ddca6-2c35-45c7-xmpl-c3a03a31ed15'
]
},
responseContext: {
statusCode: 200,
executedVersion: '$LATEST',
functionError: 'Unhandled'
},
responsePayload: {
errorMessage:
'RequestId: e4b46cbf-b738-xmpl-8880-a18cdf61200e Process exited before completing request'
}
}

module.exports = {
gatewayV1Event,
gatewayV2Event,
lambaV1InvocationEvent
}
24 changes: 24 additions & 0 deletions test/unit/serverless/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const test = require('node:test')
const assert = require('node:assert')

const { isGatewayV1Event, isGatewayV2Event } = require('../../../lib/serverless/api-gateway')
const { gatewayV1Event, gatewayV2Event, lambaV1InvocationEvent } = require('./fixtures')

test('isGatewayV1Event', () => {
assert.equal(isGatewayV1Event(gatewayV1Event), true)
assert.equal(isGatewayV1Event(gatewayV2Event), false)
assert.equal(isGatewayV1Event(lambaV1InvocationEvent), false)
})

test('isGatewayV2Event', () => {
assert.equal(isGatewayV2Event(gatewayV1Event), false)
assert.equal(isGatewayV2Event(gatewayV2Event), true)
assert.equal(isGatewayV2Event(lambaV1InvocationEvent), false)
})
Loading