Skip to content

Bug: duplicate query string parameters when multiple occurrences are present #4750

@dreamorosi

Description

@dreamorosi

Expected Behavior

When using the Event Handler with REST API Gateway V1 and multi-value query parameters (e.g., ?filter=active&filter=published), the handler should:

  1. Extract all parameter values in the correct order: ['active', 'published']
  2. Not duplicate any values
  3. Match the behavior documented in AWS API Gateway where multiValueQueryStringParameters contains all values in order

Current Behavior

The Event Handler duplicates multi-value query parameters.

For example, with the URL ?filter=active&filter=published:

  • Expected: ['active', 'published']
  • Actual: ['published', 'active', 'published']

This happens, I think, because the converter in packages/event-handler/src/rest/converters.ts (lines 74-86) processes BOTH queryStringParameters AND multiValueQueryStringParameters from the API Gateway V1 event, but according to AWS documentation:

  • queryStringParameters contains the last value of each parameter
  • multiValueQueryStringParameters contains all values for each parameter

When both fields contain the same parameter, the current implementation appends values from both, causing duplication.

Code snippet

Failing E2E test:

// packages/event-handler/tests/e2e/restEventHandler.test.ts (line 206)
it('handles array query parameters', async () => {
  const searchQuery = 'test';
  const filters = ['active', 'published'];

  const response = await fetch(
    `${apiUrl}/params/search?q=${searchQuery}&filter=${filters[0]}&filter=${filters[1]}`
  );
  const data = await response.json();

  expect(response.status).toBe(200);
  expect(data.query).toBe(searchQuery);
  expect(data.filters).toEqual(['active', 'published']); // FAILS
});

Handler implementation:

// packages/event-handler/tests/e2e/routers/paramsRouter.ts
paramsRouter.get('/search', ({ req }) => {
  const url = new URL(req.url);
  const filters = url.searchParams.getAll('filter');
  return { filters: filters.length > 0 ? filters : undefined };
});

Failing unit test demonstrating the bug:

// Add to packages/event-handler/tests/unit/rest/converters.test.ts after line 299
it('handles same parameter in both queryStringParameters and multiValueQueryStringParameters without duplication', () => {
  // Prepare
  // This simulates real API Gateway V1 behavior where multi-value query params
  // appear in BOTH fields:
  // - queryStringParameters contains the LAST value
  // - multiValueQueryStringParameters contains ALL values
  // Example: ?filter=active&filter=published
  const event = {
    ...baseEvent,
    queryStringParameters: {
      filter: 'published', // Last value (API Gateway behavior)
    },
    multiValueQueryStringParameters: {
      filter: ['active', 'published'], // All values (API Gateway behavior)
    },
  };

  // Act
  const request = proxyEventToWebRequest(event);

  // Assess
  expect(request).toBeInstanceOf(Request);
  const url = new URL(request.url);
  // Should NOT have duplicates - should only be ['active', 'published']
  // BUG: Currently returns ['published', 'active', 'published'] due to processing both
fields
  expect(url.searchParams.getAll('filter')).toEqual(['active', 'published']);
});

Steps to Reproduce

  1. Create a REST API (not HTTP API) using API Gateway V1 with Lambda proxy integration
  2. Use the Event Handler experimental-rest module
  3. Send a request with multi-value query parameters: GET /search?filter=active&filter=published
  4. Extract parameters using url.searchParams.getAll('filter')
  5. Observe that the array contains duplicates and wrong order: ['published', 'active', 'published']

Alternatively, paste the unit test shown above and run the tests locally

Possible Solution

The converter should skip parameters in queryStringParameters if they exist in multiValueQueryStringParameters, similar to how the header deduplication logic works (lines 61-68).

Proposed fix in packages/event-handler/src/rest/converters.ts:

const url = new URL(path, `${protocol}://${hostname}/`);

// Only process queryStringParameters for params NOT in multiValueQueryStringParameters
for (const [name, value] of Object.entries(
  event.queryStringParameters ?? {}
)) {
  // Skip if this parameter exists in multiValueQueryStringParameters
  if (
    value != null &&
    !event.multiValueQueryStringParameters?.[name]
  ) {
    url.searchParams.append(name, value);
  }
}

// Process multi-value query string parameters (these take precedence)
for (const [name, values] of Object.entries(
  event.multiValueQueryStringParameters ?? {}
)) {
  for (const value of values ?? []) {
    url.searchParams.append(name, value);
  }
}

Powertools for AWS Lambda (TypeScript) version

latest

AWS Lambda function runtime

22.x

Packaging format used

npm

Execution logs

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingevent-handlerThis item relates to the Event Handler Utilitypending-releaseThis item has been merged and will be released soon

Type

No type

Projects

Status

Coming soon

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions