Skip to content

Commit 5d3cf2d

Browse files
authored
fix(event-handler): handle repeated queryString values (#4755)
1 parent db7bdcc commit 5d3cf2d

File tree

2 files changed

+85
-12
lines changed

2 files changed

+85
-12
lines changed

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

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,15 @@ const createBody = (body: string | null, isBase64Encoded: boolean) => {
4444
};
4545

4646
/**
47-
* Converts an API Gateway proxy event to a Web API Request object.
47+
* Populates headers from single and multi-value header entries.
4848
*
49+
* @param headers - The Headers object to populate
4950
* @param event - The API Gateway proxy event
50-
* @returns A Web API Request object
5151
*/
52-
const proxyEventV1ToWebRequest = (event: APIGatewayProxyEvent): Request => {
53-
const { httpMethod, path } = event;
54-
const { domainName } = event.requestContext;
55-
56-
const headers = new Headers();
52+
const populateV1Headers = (
53+
headers: Headers,
54+
event: APIGatewayProxyEvent
55+
): void => {
5756
for (const [name, value] of Object.entries(event.headers ?? {})) {
5857
if (value !== undefined) headers.set(name, value);
5958
}
@@ -66,15 +65,21 @@ const proxyEventV1ToWebRequest = (event: APIGatewayProxyEvent): Request => {
6665
}
6766
}
6867
}
69-
const hostname = headers.get('Host') ?? domainName;
70-
const protocol = headers.get('X-Forwarded-Proto') ?? 'https';
71-
72-
const url = new URL(path, `${protocol}://${hostname}/`);
68+
};
7369

70+
/**
71+
* Populates URL search parameters from single and multi-value query string parameters.
72+
*
73+
* @param url - The URL object to populate
74+
* @param event - The API Gateway proxy event
75+
*/
76+
const populateV1QueryParams = (url: URL, event: APIGatewayProxyEvent): void => {
7477
for (const [name, value] of Object.entries(
7578
event.queryStringParameters ?? {}
7679
)) {
77-
if (value != null) url.searchParams.append(name, value);
80+
if (value != null && !event.multiValueQueryStringParameters?.[name]) {
81+
url.searchParams.append(name, value);
82+
}
7883
}
7984

8085
for (const [name, values] of Object.entries(
@@ -84,6 +89,27 @@ const proxyEventV1ToWebRequest = (event: APIGatewayProxyEvent): Request => {
8489
url.searchParams.append(name, value);
8590
}
8691
}
92+
};
93+
94+
/**
95+
* Converts an API Gateway proxy event to a Web API Request object.
96+
*
97+
* @param event - The API Gateway proxy event
98+
* @returns A Web API Request object
99+
*/
100+
const proxyEventV1ToWebRequest = (event: APIGatewayProxyEvent): Request => {
101+
const { httpMethod, path } = event;
102+
const { domainName } = event.requestContext;
103+
104+
const headers = new Headers();
105+
populateV1Headers(headers, event);
106+
107+
const hostname = headers.get('Host') ?? domainName;
108+
const protocol = headers.get('X-Forwarded-Proto') ?? 'https';
109+
110+
const url = new URL(path, `${protocol}://${hostname}/`);
111+
populateV1QueryParams(url, event);
112+
87113
return new Request(url.toString(), {
88114
method: httpMethod,
89115
headers,

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,53 @@ describe('Converters', () => {
298298
expect(url.searchParams.getAll('multi')).toEqual(['value1', 'value2']);
299299
});
300300

301+
it('handles same parameter in both queryStringParameters and multiValueQueryStringParameters without duplication', () => {
302+
// Prepare
303+
const event = {
304+
...baseEvent,
305+
queryStringParameters: {
306+
filter: 'published', // Last value (API Gateway behavior)
307+
},
308+
multiValueQueryStringParameters: {
309+
filter: ['active', 'published'], // All values (API Gateway behavior)
310+
},
311+
};
312+
313+
// Act
314+
const request = proxyEventToWebRequest(event);
315+
316+
// Assess
317+
expect(request).toBeInstanceOf(Request);
318+
const url = new URL(request.url);
319+
expect(url.searchParams.getAll('filter')).toEqual([
320+
'active',
321+
'published',
322+
]);
323+
});
324+
325+
it('handles mixed single and multi-value query parameters correctly', () => {
326+
// Prepare
327+
const event = {
328+
...baseEvent,
329+
queryStringParameters: {
330+
single: 'value1', // Only in single-value
331+
multi: 'last', // Also in multi-value (should be ignored)
332+
},
333+
multiValueQueryStringParameters: {
334+
multi: ['first', 'last'], // Should take precedence
335+
},
336+
};
337+
338+
// Act
339+
const request = proxyEventToWebRequest(event);
340+
341+
// Assess
342+
expect(request).toBeInstanceOf(Request);
343+
const url = new URL(request.url);
344+
expect(url.searchParams.get('single')).toBe('value1');
345+
expect(url.searchParams.getAll('multi')).toEqual(['first', 'last']);
346+
});
347+
301348
it('skips undefined queryStringParameter values', () => {
302349
// Prepare
303350
const event = {

0 commit comments

Comments
 (0)