Skip to content

Commit 5e8b260

Browse files
authored
fix: Improved AWS Lambda event detection (#2498)
1 parent c395779 commit 5e8b260

File tree

5 files changed

+273
-114
lines changed

5 files changed

+273
-114
lines changed

lib/serverless/api-gateway.js

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -106,38 +106,45 @@ function isLambdaProxyEvent(event) {
106106
return isGatewayV1Event(event) || isGatewayV2Event(event)
107107
}
108108

109-
function isGatewayV1Event(event) {
110-
let result = false
111-
112-
if (event?.version === '1.0') {
113-
result = true
114-
} else if (
115-
typeof event?.path === 'string' &&
116-
(event.headers ?? event.multiValueHeaders) &&
117-
typeof event?.httpMethod === 'string'
118-
// eslint-disable-next-line sonarjs/no-duplicated-branches
119-
) {
120-
result = true
121-
}
109+
const v1Keys = [
110+
'body',
111+
'headers',
112+
'httpMethod',
113+
'isBase64Encoded',
114+
'multiValueHeaders',
115+
'multiValueQueryStringParameters',
116+
'path',
117+
'pathParameters',
118+
'queryStringParameters',
119+
'requestContext',
120+
'resource',
121+
'stageVariables',
122+
'version'
123+
].join(',')
122124

123-
return result
125+
function isGatewayV1Event(event) {
126+
const keys = Object.keys(event).sort().join(',')
127+
return keys === v1Keys && event?.version === '1.0'
124128
}
125129

126-
function isGatewayV2Event(event) {
127-
let result = false
128-
129-
if (event?.version === '2.0') {
130-
result = true
131-
} else if (
132-
typeof event?.requestContext?.http?.path === 'string' &&
133-
Object.prototype.toString.call(event.headers) === '[object Object]' &&
134-
typeof event?.requestContext?.http?.method === 'string'
135-
// eslint-disable-next-line sonarjs/no-duplicated-branches
136-
) {
137-
result = true
138-
}
130+
const v2Keys = [
131+
'body',
132+
'cookies',
133+
'headers',
134+
'isBase64Encoded',
135+
'pathParameters',
136+
'queryStringParameters',
137+
'rawPath',
138+
'rawQueryString',
139+
'requestContext',
140+
'routeKey',
141+
'stageVariables',
142+
'version'
143+
].join(',')
139144

140-
return result
145+
function isGatewayV2Event(event) {
146+
const keys = Object.keys(event).sort().join(',')
147+
return keys === v2Keys && event?.version === '2.0'
141148
}
142149

143150
/**
@@ -155,5 +162,7 @@ module.exports = {
155162
LambdaProxyWebRequest,
156163
LambdaProxyWebResponse,
157164
isLambdaProxyEvent,
158-
isValidLambdaProxyResponse
165+
isValidLambdaProxyResponse,
166+
isGatewayV1Event,
167+
isGatewayV2Event
159168
}

test/unit/serverless/api-gateway-v2.test.js

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,73 +13,7 @@ const AwsLambda = require('../../../lib/serverless/aws-lambda')
1313

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

16-
// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
17-
const v2Event = {
18-
version: '2.0',
19-
routeKey: '$default',
20-
rawPath: '/my/path',
21-
rawQueryString: 'parameter1=value1&parameter1=value2&parameter2=value',
22-
cookies: ['cookie1', 'cookie2'],
23-
headers: {
24-
header1: 'value1',
25-
header2: 'value1,value2',
26-
accept: 'application/json'
27-
},
28-
queryStringParameters: {
29-
parameter1: 'value1,value2',
30-
parameter2: 'value',
31-
name: 'me',
32-
team: 'node agent'
33-
},
34-
requestContext: {
35-
accountId: '123456789012',
36-
apiId: 'api-id',
37-
authentication: {
38-
clientCert: {
39-
clientCertPem: 'CERT_CONTENT',
40-
subjectDN: 'www.example.com',
41-
issuerDN: 'Example issuer',
42-
serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1',
43-
validity: {
44-
notBefore: 'May 28 12:30:02 2019 GMT',
45-
notAfter: 'Aug 5 09:36:04 2021 GMT'
46-
}
47-
}
48-
},
49-
authorizer: {
50-
jwt: {
51-
claims: {
52-
claim1: 'value1',
53-
claim2: 'value2'
54-
},
55-
scopes: ['scope1', 'scope2']
56-
}
57-
},
58-
domainName: 'id.execute-api.us-east-1.amazonaws.com',
59-
domainPrefix: 'id',
60-
http: {
61-
method: 'POST',
62-
path: '/my/path',
63-
protocol: 'HTTP/1.1',
64-
sourceIp: '192.0.2.1',
65-
userAgent: 'agent'
66-
},
67-
requestId: 'id',
68-
routeKey: '$default',
69-
stage: '$default',
70-
time: '12/Mar/2020:19:03:58 +0000',
71-
timeEpoch: 1583348638390
72-
},
73-
body: 'Hello from Lambda',
74-
pathParameters: {
75-
parameter1: 'value1'
76-
},
77-
isBase64Encoded: false,
78-
stageVariables: {
79-
stageVariable1: 'value1',
80-
stageVariable2: 'value2'
81-
}
82-
}
16+
const { gatewayV2Event: v2Event } = require('./fixtures')
8317

8418
tap.beforeEach((t) => {
8519
// This env var suppresses console output we don't need to inspect.

test/unit/serverless/fixtures.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
9+
const gatewayV1Event = {
10+
version: '1.0',
11+
resource: '/my/path',
12+
path: '/my/path',
13+
httpMethod: 'GET',
14+
headers: {
15+
header1: 'value1',
16+
header2: 'value2'
17+
},
18+
multiValueHeaders: {
19+
header1: ['value1'],
20+
header2: ['value1', 'value2']
21+
},
22+
queryStringParameters: {
23+
parameter1: 'value1',
24+
parameter2: 'value'
25+
},
26+
multiValueQueryStringParameters: {
27+
parameter1: ['value1', 'value2'],
28+
parameter2: ['value']
29+
},
30+
requestContext: {
31+
accountId: '123456789012',
32+
apiId: 'id',
33+
authorizer: {
34+
claims: null,
35+
scopes: null
36+
},
37+
domainName: 'id.execute-api.us-east-1.amazonaws.com',
38+
domainPrefix: 'id',
39+
extendedRequestId: 'request-id',
40+
httpMethod: 'GET',
41+
identity: {
42+
accessKey: null,
43+
accountId: null,
44+
caller: null,
45+
cognitoAuthenticationProvider: null,
46+
cognitoAuthenticationType: null,
47+
cognitoIdentityId: null,
48+
cognitoIdentityPoolId: null,
49+
principalOrgId: null,
50+
sourceIp: '192.0.2.1',
51+
user: null,
52+
userAgent: 'user-agent',
53+
userArn: null,
54+
clientCert: {
55+
clientCertPem: 'CERT_CONTENT',
56+
subjectDN: 'www.example.com',
57+
issuerDN: 'Example issuer',
58+
serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1',
59+
validity: {
60+
notBefore: 'May 28 12:30:02 2019 GMT',
61+
notAfter: 'Aug 5 09:36:04 2021 GMT'
62+
}
63+
}
64+
},
65+
path: '/my/path',
66+
protocol: 'HTTP/1.1',
67+
requestId: 'id=',
68+
requestTime: '04/Mar/2020:19:15:17 +0000',
69+
requestTimeEpoch: 1583349317135,
70+
resourceId: null,
71+
resourcePath: '/my/path',
72+
stage: '$default'
73+
},
74+
pathParameters: null,
75+
stageVariables: null,
76+
body: 'Hello from Lambda!',
77+
isBase64Encoded: false
78+
}
79+
80+
// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
81+
const gatewayV2Event = {
82+
version: '2.0',
83+
routeKey: '$default',
84+
rawPath: '/my/path',
85+
rawQueryString: 'parameter1=value1&parameter1=value2&parameter2=value',
86+
cookies: ['cookie1', 'cookie2'],
87+
headers: {
88+
header1: 'value1',
89+
header2: 'value1,value2',
90+
accept: 'application/json'
91+
},
92+
queryStringParameters: {
93+
parameter1: 'value1,value2',
94+
parameter2: 'value',
95+
name: 'me',
96+
team: 'node agent'
97+
},
98+
requestContext: {
99+
accountId: '123456789012',
100+
apiId: 'api-id',
101+
authentication: {
102+
clientCert: {
103+
clientCertPem: 'CERT_CONTENT',
104+
subjectDN: 'www.example.com',
105+
issuerDN: 'Example issuer',
106+
serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1',
107+
validity: {
108+
notBefore: 'May 28 12:30:02 2019 GMT',
109+
notAfter: 'Aug 5 09:36:04 2021 GMT'
110+
}
111+
}
112+
},
113+
authorizer: {
114+
jwt: {
115+
claims: {
116+
claim1: 'value1',
117+
claim2: 'value2'
118+
},
119+
scopes: ['scope1', 'scope2']
120+
}
121+
},
122+
domainName: 'id.execute-api.us-east-1.amazonaws.com',
123+
domainPrefix: 'id',
124+
http: {
125+
method: 'POST',
126+
path: '/my/path',
127+
protocol: 'HTTP/1.1',
128+
sourceIp: '192.0.2.1',
129+
userAgent: 'agent'
130+
},
131+
requestId: 'id',
132+
routeKey: '$default',
133+
stage: '$default',
134+
time: '12/Mar/2020:19:03:58 +0000',
135+
timeEpoch: 1583348638390
136+
},
137+
body: 'Hello from Lambda',
138+
pathParameters: {
139+
parameter1: 'value1'
140+
},
141+
isBase64Encoded: false,
142+
stageVariables: {
143+
stageVariable1: 'value1',
144+
stageVariable2: 'value2'
145+
}
146+
}
147+
148+
// Event used when one Lambda directly invokes another Lambda.
149+
// https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-async-destinations
150+
const lambaV1InvocationEvent = {
151+
version: '1.0',
152+
timestamp: '2019-11-14T18:16:05.568Z',
153+
requestContext: {
154+
requestId: 'e4b46cbf-b738-xmpl-8880-a18cdf61200e',
155+
functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:$LATEST',
156+
condition: 'RetriesExhausted',
157+
approximateInvokeCount: 3
158+
},
159+
requestPayload: {
160+
ORDER_IDS: [
161+
'9e07af03-ce31-4ff3-xmpl-36dce652cb4f',
162+
'637de236-e7b2-464e-xmpl-baf57f86bb53',
163+
'a81ddca6-2c35-45c7-xmpl-c3a03a31ed15'
164+
]
165+
},
166+
responseContext: {
167+
statusCode: 200,
168+
executedVersion: '$LATEST',
169+
functionError: 'Unhandled'
170+
},
171+
responsePayload: {
172+
errorMessage:
173+
'RequestId: e4b46cbf-b738-xmpl-8880-a18cdf61200e Process exited before completing request'
174+
}
175+
}
176+
177+
module.exports = {
178+
gatewayV1Event,
179+
gatewayV2Event,
180+
lambaV1InvocationEvent
181+
}

0 commit comments

Comments
 (0)