-
Notifications
You must be signed in to change notification settings - Fork 469
feat: Add a flag to ALBResolver to URL-decode query parameters #7940
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3316,8 +3316,34 @@ def __init__( | |
| enable_validation: bool = False, | ||
| response_validation_error_http_code: HTTPStatus | int | None = None, | ||
| json_body_deserializer: Callable[[str], dict] | None = None, | ||
| decode_query_parameters: bool = False, | ||
| ): | ||
| """Amazon Application Load Balancer (ALB) resolver""" | ||
| """Amazon Application Load Balancer (ALB) resolver | ||
|
|
||
|
|
||
| Parameters | ||
| ---------- | ||
| cors: CORSConfig | ||
| Optionally configure and enabled CORS. Not each route will need to have to cors=True | ||
| debug: bool | None | ||
| Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_DEV" | ||
| environment variable | ||
| serializer: Callable, optional | ||
| function to serialize `obj` to a JSON formatted `str`, by default json.dumps | ||
| strip_prefixes: list[str | Pattern], optional | ||
| optional list of prefixes to be removed from the request path before doing the routing. | ||
| This is often used with api gateways with multiple custom mappings. | ||
| Each prefix can be a static string or a compiled regex pattern | ||
| enable_validation: bool | None | ||
| Enables validation of the request body against the route schema, by default False. | ||
| response_validation_error_http_code | ||
| Sets the returned status code if response is not validated. enable_validation must be True. | ||
| json_body_deserializer: Callable[[str], dict], optional | ||
| function to deserialize `str`, `bytes`, `bytearray` containing a JSON document to a Python `dict`, | ||
| by default json.loads when integrating with EventSource data class | ||
| decode_query_parameters: bool | None | ||
| Enables URL-decoding of query parameters (both keys and values), by default False. | ||
| """ | ||
| super().__init__( | ||
| ProxyEventType.ALBEvent, | ||
| cors, | ||
|
|
@@ -3328,6 +3354,7 @@ def __init__( | |
| response_validation_error_http_code, | ||
| json_body_deserializer=json_body_deserializer, | ||
| ) | ||
| self.decode_query_parameters = decode_query_parameters | ||
|
|
||
| def _get_base_path(self) -> str: | ||
| # ALB doesn't have a stage variable, so we just return an empty string | ||
|
|
@@ -3354,3 +3381,10 @@ def _to_response(self, result: dict | tuple | Response | BedrockResponse) -> Res | |
| result.body = "" | ||
|
|
||
| return super()._to_response(result) | ||
|
|
||
| @override | ||
| def _to_proxy_event(self, event: dict) -> BaseProxyEvent: | ||
| proxy_event = super()._to_proxy_event(event) | ||
| if isinstance(proxy_event, ALBEvent): | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should always be the case, but I think mypy would complain if I didn't. Alternatively, I could just create the ALBEvent here, but if the base method ever changes, that could break.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coverage is complaining about this, since I don't test the negative branch. Is a |
||
| proxy_event.decode_query_parameters = self.decode_query_parameters | ||
| return proxy_event | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
| from typing import Any, Callable | ||
| from urllib.parse import unquote | ||
|
|
||
| from typing_extensions import override | ||
|
|
||
| from aws_lambda_powertools.shared.headers_serializer import ( | ||
| BaseHeadersSerializer, | ||
|
|
@@ -30,13 +33,27 @@ class ALBEvent(BaseProxyEvent): | |
| - https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html | ||
| """ | ||
|
|
||
| @override | ||
| def __init__(self, data: dict[str, Any], json_deserializer: Callable | None = None): | ||
| super().__init__(data, json_deserializer) | ||
| self.decode_query_parameters = False | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This gets overridden in ALBResolver._to_proxy_event. |
||
|
|
||
| @property | ||
| def request_context(self) -> ALBEventRequestContext: | ||
| return ALBEventRequestContext(self["requestContext"]) | ||
|
|
||
| @property | ||
| def resolved_query_string_parameters(self) -> dict[str, list[str]]: | ||
| return self.multi_value_query_string_parameters or super().resolved_query_string_parameters | ||
| params = self.multi_value_query_string_parameters or super().resolved_query_string_parameters | ||
| if not self.decode_query_parameters: | ||
| return params | ||
|
|
||
| # Decode the parameter keys and values | ||
| decoded_params = {} | ||
| for k, vals in params.items(): | ||
| decoded_params[unquote(k)] = [unquote(v) for v in vals] | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decoded the keys for consistency. That's what it looks like other frameworks do, although I'm not sure it would be useful here. |
||
|
|
||
| return decoded_params | ||
|
|
||
| @property | ||
| def multi_value_headers(self) -> dict[str, list[str]]: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import base64 | ||
| import datetime | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the best file for this test? I originally started here, but not sure if something like |
||
| import json | ||
| from dataclasses import dataclass | ||
| from enum import Enum | ||
|
|
@@ -20,6 +21,7 @@ | |
| ) | ||
| from aws_lambda_powertools.event_handler.openapi.exceptions import ResponseValidationError | ||
| from aws_lambda_powertools.event_handler.openapi.params import Body, Form, Header, Query | ||
| from tests.functional.utils import load_event | ||
|
|
||
|
|
||
| def test_validate_scalars(gw_event): | ||
|
|
@@ -1070,6 +1072,26 @@ def handler3(): | |
| assert any(text in result["body"] for text in expected_error_text) | ||
|
|
||
|
|
||
| def test_validation_query_string_with_encoded_datetime_alb_resolver(): | ||
| # GIVEN a ALBResolver with validation enabled, | ||
| # and an event with a url-encoded datetime | ||
| # as a query string parameter | ||
| app = ALBResolver(enable_validation=True, decode_query_parameters=True) | ||
| raw_event = load_event("albEvent.json") | ||
| raw_event["path"] = "/users" | ||
| raw_event["queryStringParameters"] = {"query_dt": "2025-12-20T16%3A56%3A02.032000"} | ||
|
|
||
| # WHEN a handler is defined with various parameters and routes | ||
| @app.get("/users") | ||
| def handler(query_dt: datetime.datetime): | ||
| return None | ||
|
|
||
| # THEN the handler should be invoked with the expected result | ||
| # AND the status code should match the expected_status_code | ||
| result = app(raw_event, {}) | ||
| assert result["statusCode"] == 200 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "handler_func, expected_status_code, expected_error_text", | ||
| [ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are copied from
ApiGatewayResolver(except fordecode_query_parameters, obviously)