From ff45704c8c488a9229063dd6bacead09d9f4f454 Mon Sep 17 00:00:00 2001 From: DYNOSuprovo Date: Sat, 3 Oct 2026 21:25:51 +0530 Subject: [PATCH] fix(event_handler): preserve comma-separated values in scalar parameters and model fields (#8508) Signed-off-by: DYNOSuprovo --- .../middlewares/openapi_validation.py | 11 ++- .../test_openapi_validation_middleware.py | 70 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py index a0311184a51..13559726efc 100644 --- a/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py +++ b/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py @@ -540,6 +540,8 @@ def _normalize_field_value(value: Any, field_info: FieldInfo) -> Any: if _is_or_contains_sequence(field_info.annotation): return value elif isinstance(value, list) and value: + if all(isinstance(v, str) for v in value): + return ",".join(value) return value[0] return value @@ -611,11 +613,14 @@ def _normalize_multi_params( def _process_scalar_param(input_dict: MutableMapping[str, Any], param: ModelField) -> None: - """Process a scalar parameter by normalizing single-item lists.""" + """Process a scalar parameter by normalizing single-item lists or rejoining split strings.""" try: value = input_dict[param.alias] - if isinstance(value, list) and len(value) == 1: - input_dict[param.alias] = value[0] + if isinstance(value, list) and value: + if len(value) == 1: + input_dict[param.alias] = value[0] + elif all(isinstance(v, str) for v in value): + input_dict[param.alias] = ",".join(value) except KeyError: pass diff --git a/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py b/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py index ec14c14fa08..5c38cb48746 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_validation_middleware.py @@ -4355,3 +4355,73 @@ def handler() -> Response[None]: result = app(gw_event_alb, {}) assert result["statusCode"] == 204 assert result["body"] == "" + + +def test_scalar_query_parameter_with_commas(gw_event_http): + # GIVEN an APIGatewayHttpResolver with validation enabled + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/search") + def handler(search: Annotated[str, Query()]): + return {"search": search} + + gw_event_http["rawPath"] = "/search" + gw_event_http["requestContext"]["http"]["method"] = "GET" + gw_event_http["rawQueryString"] = "search=hello,world" + gw_event_http["queryStringParameters"] = {"search": "hello,world"} + + # WHEN requesting with comma-separated query parameter value + result = app(gw_event_http, {}) + + # THEN the scalar string parameter should preserve the comma-separated value + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["search"] == "hello,world" + + +def test_header_parameter_with_commas(gw_event_http): + # GIVEN an APIGatewayHttpResolver with validation enabled + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/client-ip") + def handler(x_forwarded_for: Annotated[str, Header()]): + return {"x_forwarded_for": x_forwarded_for} + + gw_event_http["rawPath"] = "/client-ip" + gw_event_http["requestContext"]["http"]["method"] = "GET" + gw_event_http["headers"]["x-forwarded-for"] = "203.0.113.9, 10.0.0.1" + + # WHEN requesting with comma-separated header value + result = app(gw_event_http, {}) + + # THEN the scalar string header should preserve the comma-separated value + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["x_forwarded_for"] == "203.0.113.9, 10.0.0.1" + + +def test_model_with_comma_separated_fields(gw_event_http): + # GIVEN a model with both scalar string and list sequence fields + class SearchParams(BaseModel): + search: str + tags: List[str] + + app = APIGatewayHttpResolver(enable_validation=True) + + @app.get("/items") + def handler(params: Annotated[SearchParams, Query()]): + return {"search": params.search, "tags": params.tags} + + gw_event_http["rawPath"] = "/items" + gw_event_http["requestContext"]["http"]["method"] = "GET" + gw_event_http["rawQueryString"] = "search=hello,world&tags=a,b" + gw_event_http["queryStringParameters"] = {"search": "hello,world", "tags": "a,b"} + + # WHEN requesting with comma values for both scalar and sequence fields + result = app(gw_event_http, {}) + + # THEN the scalar field preserves commas and sequence field is parsed as list + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["search"] == "hello,world" + assert body["tags"] == ["a", "b"]