Skip to content

Commit 58f5ddb

Browse files
committed
feat: Add a flag to ALBResolver to URL-decode query parameters
1 parent 139107a commit 58f5ddb

File tree

4 files changed

+83
-2
lines changed

4 files changed

+83
-2
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3154,6 +3154,7 @@ def __init__(
31543154
enable_validation: bool = False,
31553155
response_validation_error_http_code: HTTPStatus | int | None = None,
31563156
json_body_deserializer: Callable[[str], dict] | None = None,
3157+
decode_query_parameters: bool = False,
31573158
):
31583159
"""Amazon Application Load Balancer (ALB) resolver"""
31593160
super().__init__(
@@ -3166,6 +3167,7 @@ def __init__(
31663167
response_validation_error_http_code,
31673168
json_body_deserializer=json_body_deserializer,
31683169
)
3170+
self.decode_query_parameters = decode_query_parameters
31693171

31703172
def _get_base_path(self) -> str:
31713173
# ALB doesn't have a stage variable, so we just return an empty string
@@ -3192,3 +3194,10 @@ def _to_response(self, result: dict | tuple | Response | BedrockResponse) -> Res
31923194
result.body = ""
31933195

31943196
return super()._to_response(result)
3197+
3198+
@override
3199+
def _to_proxy_event(self, event: dict) -> BaseProxyEvent:
3200+
proxy_event = super()._to_proxy_event(event)
3201+
if isinstance(proxy_event, ALBEvent):
3202+
proxy_event.decode_query_parameters = self.decode_query_parameters
3203+
return proxy_event

aws_lambda_powertools/utilities/data_classes/alb_event.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import annotations
22

3-
from typing import Any
3+
from typing import Any, Callable
4+
from urllib.parse import unquote
5+
6+
from typing_extensions import override
47

58
from aws_lambda_powertools.shared.headers_serializer import (
69
BaseHeadersSerializer,
@@ -30,13 +33,27 @@ class ALBEvent(BaseProxyEvent):
3033
- https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html
3134
"""
3235

36+
@override
37+
def __init__(self, data: dict[str, Any], json_deserializer: Callable | None = None):
38+
super().__init__(data, json_deserializer)
39+
self.decode_query_parameters = False
40+
3341
@property
3442
def request_context(self) -> ALBEventRequestContext:
3543
return ALBEventRequestContext(self["requestContext"])
3644

3745
@property
3846
def resolved_query_string_parameters(self) -> dict[str, list[str]]:
39-
return self.multi_value_query_string_parameters or super().resolved_query_string_parameters
47+
params = self.multi_value_query_string_parameters or super().resolved_query_string_parameters
48+
if not self.decode_query_parameters:
49+
return params
50+
51+
# Decode the parameter keys and values
52+
decoded_params = {}
53+
for k, vals in params.items():
54+
decoded_params[unquote(k)] = [unquote(v) for v in vals]
55+
56+
return decoded_params
4057

4158
@property
4259
def multi_value_headers(self) -> dict[str, list[str]]:

tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import datetime
23
import json
34
from dataclasses import dataclass
45
from enum import Enum
@@ -20,6 +21,7 @@
2021
)
2122
from aws_lambda_powertools.event_handler.openapi.exceptions import ResponseValidationError
2223
from aws_lambda_powertools.event_handler.openapi.params import Body, Form, Header, Query
24+
from tests.functional.utils import load_event
2325

2426

2527
def test_validate_scalars(gw_event):
@@ -1070,6 +1072,26 @@ def handler3():
10701072
assert any(text in result["body"] for text in expected_error_text)
10711073

10721074

1075+
def test_validation_query_string_with_encoded_datetime_alb_resolver():
1076+
# GIVEN a ALBResolver with validation enabled,
1077+
# and an event with a url-encoded datetime
1078+
# as a query string parameter
1079+
app = ALBResolver(enable_validation=True, decode_query_parameters=True)
1080+
raw_event = load_event("albEvent.json")
1081+
raw_event["path"] = "/users"
1082+
raw_event["queryStringParameters"] = {"query_dt": "2025-12-20T16%3A56%3A02.032000"}
1083+
1084+
# WHEN a handler is defined with various parameters and routes
1085+
@app.get("/users")
1086+
def handler(query_dt: datetime.datetime):
1087+
return None
1088+
1089+
# THEN the handler should be invoked with the expected result
1090+
# AND the status code should match the expected_status_code
1091+
result = app(raw_event, {})
1092+
assert result["statusCode"] == 200
1093+
1094+
10731095
@pytest.mark.parametrize(
10741096
"handler_func, expected_status_code, expected_error_text",
10751097
[

tests/unit/data_classes/required_dependencies/test_alb_event.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from urllib.parse import quote
4+
35
from aws_lambda_powertools.utilities.data_classes import ALBEvent
46
from tests.functional.utils import load_event
57

@@ -19,3 +21,34 @@ def test_alb_event():
1921
assert parsed_event.multi_value_headers == (raw_event.get("multiValueHeaders") or {})
2022
assert parsed_event.body == raw_event["body"]
2123
assert parsed_event.is_base64_encoded == raw_event["isBase64Encoded"]
24+
25+
26+
def test_alb_event_decode_query_parameters():
27+
expected_key = "this is a key"
28+
expected_value = "single value"
29+
raw_event = load_event("albEvent.json")
30+
raw_event["queryStringParameters"] = {quote(expected_key): quote(expected_value)}
31+
# Without decode_query_parameters, the key and value are not decoded
32+
parsed_event = ALBEvent(raw_event)
33+
assert parsed_event.resolved_query_string_parameters != {expected_key: [expected_value]}
34+
assert parsed_event.resolved_query_string_parameters == {quote(expected_key): [quote(expected_value)]}
35+
36+
# With decode_query_parameters, the key and value are not decoded
37+
parsed_event.decode_query_parameters = True
38+
assert parsed_event.resolved_query_string_parameters == {expected_key: [expected_value]}
39+
40+
41+
def test_alb_event_decode_multi_value_query_parameters():
42+
expected_key = "this is a key"
43+
expected_values = ["first value", "second value"]
44+
45+
raw_event = load_event("albMultiValueQueryStringEvent.json")
46+
raw_event["multiValueQueryStringParameters"] = {quote(expected_key): [quote(v) for v in expected_values]}
47+
# Without decode_query_parameters, the key and value are not decoded
48+
parsed_event = ALBEvent(raw_event)
49+
assert parsed_event.resolved_query_string_parameters != {expected_key: expected_values}
50+
assert parsed_event.resolved_query_string_parameters == {quote(expected_key): [quote(v) for v in expected_values]}
51+
52+
# With decode_query_parameters, the key and value are not decoded
53+
parsed_event.decode_query_parameters = True
54+
assert parsed_event.resolved_query_string_parameters == {expected_key: expected_values}

0 commit comments

Comments
 (0)