Skip to content

Commit 200f47b

Browse files
authored
fix(event-handler): handle nullable fields in APIGatewayProxyEvent (#4455)
1 parent cf49a38 commit 200f47b

File tree

2 files changed

+124
-47
lines changed

2 files changed

+124
-47
lines changed

packages/event-handler/src/rest/utils.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,17 @@ export const isAPIGatewayProxyEvent = (
7575
isString(event.httpMethod) &&
7676
isString(event.path) &&
7777
isString(event.resource) &&
78-
isRecord(event.headers) &&
78+
(event.headers == null || isRecord(event.headers)) &&
79+
(event.multiValueHeaders == null || isRecord(event.multiValueHeaders)) &&
7980
isRecord(event.requestContext) &&
8081
typeof event.isBase64Encoded === 'boolean' &&
81-
(event.body === null || isString(event.body))
82+
(event.body === null || isString(event.body)) &&
83+
(event.pathParameters === null || isRecord(event.pathParameters)) &&
84+
(event.queryStringParameters === null ||
85+
isRecord(event.queryStringParameters)) &&
86+
(event.multiValueQueryStringParameters === null ||
87+
isRecord(event.multiValueQueryStringParameters)) &&
88+
(event.stageVariables === null || isRecord(event.stageVariables))
8289
);
8390
};
8491

packages/event-handler/tests/unit/rest/utils.test.ts

Lines changed: 115 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -214,45 +214,105 @@ describe('Path Utilities', () => {
214214
});
215215

216216
describe('isAPIGatewayProxyEvent', () => {
217-
it('should return true for valid API Gateway Proxy event', () => {
218-
const validEvent: APIGatewayProxyEvent = {
219-
httpMethod: 'GET',
217+
const baseValidEvent = {
218+
httpMethod: 'GET',
219+
path: '/test',
220+
resource: '/test',
221+
headers: {},
222+
multiValueHeaders: {},
223+
queryStringParameters: {},
224+
multiValueQueryStringParameters: {},
225+
pathParameters: {},
226+
stageVariables: {},
227+
requestContext: { stage: 'test' },
228+
isBase64Encoded: false,
229+
body: null,
230+
};
231+
232+
it('should return true for valid API Gateway Proxy event with all fields populated', () => {
233+
expect(isAPIGatewayProxyEvent(baseValidEvent)).toBe(true);
234+
});
235+
236+
it('should return true for real API Gateway event with null fields', () => {
237+
const realEvent = {
238+
resource: '/{proxy+}',
220239
path: '/test',
221-
resource: '/test',
222-
headers: {},
240+
httpMethod: 'GET',
241+
headers: null,
242+
multiValueHeaders: null,
243+
queryStringParameters: null,
244+
multiValueQueryStringParameters: null,
245+
pathParameters: { proxy: 'test' },
246+
stageVariables: null,
223247
requestContext: {
224-
accountId: '123456789012',
225-
apiId: 'test-api',
248+
resourceId: 'ovdb9g',
249+
resourcePath: '/{proxy+}',
226250
httpMethod: 'GET',
227-
requestId: 'test-request-id',
228-
resourceId: 'test-resource',
229-
resourcePath: '/test',
230-
stage: 'test',
231-
identity: {
232-
sourceIp: '127.0.0.1',
233-
},
234-
} as any,
235-
isBase64Encoded: false,
251+
stage: 'test-invoke-stage',
252+
requestId: 'eecdfcfa-225a-4ee3-bdca-05fc31b6018a',
253+
identity: { sourceIp: 'test-invoke-source-ip' },
254+
},
236255
body: null,
237-
} as APIGatewayProxyEvent;
256+
isBase64Encoded: false,
257+
};
238258

239-
expect(isAPIGatewayProxyEvent(validEvent)).toBe(true);
259+
expect(isAPIGatewayProxyEvent(realEvent)).toBe(true);
240260
});
241261

242-
it('should return true for valid event with string body', () => {
243-
const validEvent = {
262+
it('should return true for event with string body', () => {
263+
const eventWithBody = {
264+
...baseValidEvent,
244265
httpMethod: 'POST',
245-
path: '/test',
246-
resource: '/test',
247-
headers: { 'content-type': 'application/json' },
248-
requestContext: { stage: 'test' },
249-
isBase64Encoded: false,
250266
body: '{"key":"value"}',
251267
};
252268

253-
expect(isAPIGatewayProxyEvent(validEvent)).toBe(true);
269+
expect(isAPIGatewayProxyEvent(eventWithBody)).toBe(true);
254270
});
255271

272+
it.each([
273+
// Headers can be null in reality (even though types say otherwise)
274+
{ field: 'headers', value: null },
275+
{ field: 'headers', value: undefined },
276+
{ field: 'multiValueHeaders', value: null },
277+
{ field: 'multiValueHeaders', value: undefined },
278+
// These are officially nullable in the type definition
279+
{ field: 'body', value: null },
280+
{ field: 'pathParameters', value: null },
281+
{ field: 'queryStringParameters', value: null },
282+
{ field: 'multiValueQueryStringParameters', value: null },
283+
{ field: 'stageVariables', value: null },
284+
])('should return true when $field is $value', ({ field, value }) => {
285+
const event = { ...baseValidEvent, [field]: value };
286+
expect(isAPIGatewayProxyEvent(event)).toBe(true);
287+
});
288+
289+
it.each([
290+
{
291+
field: 'headers',
292+
value: { 'content-type': undefined, 'x-api-key': 'test' },
293+
},
294+
{
295+
field: 'multiValueHeaders',
296+
value: { accept: undefined, 'x-custom': ['val1', 'val2'] },
297+
},
298+
{ field: 'pathParameters', value: { id: undefined, name: 'test' } },
299+
{
300+
field: 'queryStringParameters',
301+
value: { filter: undefined, sort: 'asc' },
302+
},
303+
{
304+
field: 'multiValueQueryStringParameters',
305+
value: { tags: undefined, categories: ['a', 'b'] },
306+
},
307+
{ field: 'stageVariables', value: { env: undefined, version: 'v1' } },
308+
])(
309+
'should return true when $field contains undefined values',
310+
({ field, value }) => {
311+
const event = { ...baseValidEvent, [field]: value };
312+
expect(isAPIGatewayProxyEvent(event)).toBe(true);
313+
}
314+
);
315+
256316
it.each([
257317
{ case: 'null', event: null },
258318
{ case: 'undefined', event: undefined },
@@ -266,42 +326,52 @@ describe('Path Utilities', () => {
266326
it.each([
267327
{ field: 'httpMethod', value: 123 },
268328
{ field: 'httpMethod', value: null },
329+
{ field: 'httpMethod', value: undefined },
269330
{ field: 'path', value: 123 },
270331
{ field: 'path', value: null },
332+
{ field: 'path', value: undefined },
271333
{ field: 'resource', value: 123 },
272334
{ field: 'resource', value: null },
335+
{ field: 'resource', value: undefined },
273336
{ field: 'headers', value: 'not an object' },
274-
{ field: 'headers', value: null },
337+
{ field: 'headers', value: 123 },
338+
{ field: 'multiValueHeaders', value: 'not an object' },
339+
{ field: 'multiValueHeaders', value: 123 },
340+
{ field: 'queryStringParameters', value: 'not an object' },
341+
{ field: 'queryStringParameters', value: 123 },
342+
{ field: 'multiValueQueryStringParameters', value: 'not an object' },
343+
{ field: 'multiValueQueryStringParameters', value: 123 },
344+
{ field: 'pathParameters', value: 'not an object' },
345+
{ field: 'pathParameters', value: 123 },
346+
{ field: 'stageVariables', value: 'not an object' },
347+
{ field: 'stageVariables', value: 123 },
275348
{ field: 'requestContext', value: 'not an object' },
276349
{ field: 'requestContext', value: null },
350+
{ field: 'requestContext', value: undefined },
351+
{ field: 'requestContext', value: 123 },
277352
{ field: 'isBase64Encoded', value: 'not a boolean' },
278353
{ field: 'isBase64Encoded', value: null },
354+
{ field: 'isBase64Encoded', value: undefined },
355+
{ field: 'isBase64Encoded', value: 123 },
279356
{ field: 'body', value: 123 },
357+
{ field: 'body', value: {} },
280358
])(
281359
'should return false when $field is invalid ($value)',
282360
({ field, value }) => {
283-
const baseEvent = {
284-
httpMethod: 'GET',
285-
path: '/test',
286-
resource: '/test',
287-
headers: {},
288-
requestContext: {},
289-
isBase64Encoded: false,
290-
body: null,
291-
};
292-
293-
const invalidEvent = { ...baseEvent, [field]: value };
361+
const invalidEvent = { ...baseValidEvent, [field]: value };
294362
expect(isAPIGatewayProxyEvent(invalidEvent)).toBe(false);
295363
}
296364
);
297365

298-
it('should return false when required fields are missing', () => {
299-
const incompleteEvent = {
300-
httpMethod: 'GET',
301-
path: '/test',
302-
// missing resource, headers, requestContext, isBase64Encoded, body
303-
};
304-
366+
it.each([
367+
'httpMethod',
368+
'path',
369+
'resource',
370+
'requestContext',
371+
'isBase64Encoded',
372+
])('should return false when required field %s is missing', (field) => {
373+
const incompleteEvent = { ...baseValidEvent };
374+
delete incompleteEvent[field as keyof typeof incompleteEvent];
305375
expect(isAPIGatewayProxyEvent(incompleteEvent)).toBe(false);
306376
});
307377
});

0 commit comments

Comments
 (0)