Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -3343,22 +3343,13 @@ def _get_base_path(self) -> str:
# ALB doesn't have a stage variable, so we just return an empty string
return ""

# BedrockResponse is not used here but adding the same signature to keep strong typing
@override
def _to_response(self, result: dict | tuple | Response | BedrockResponse) -> Response | BedrockResponse:
"""Convert the route's result to a Response

ALB requires a non-null body otherwise it converts as HTTP 5xx

3 main result types are supported:

- Dict[str, Any]: Rest api response with just the Dict to json stringify and content-type is set to
application/json
- Tuple[dict, int]: Same dict handling as above but with the option of including a status code
- Response: returned as is, and allows for more flexibility
"""

# NOTE: Minor override for early return on Response with null body for ALB
# ALB doesn't support null body - convert before building the final response
if isinstance(result, Response) and result.body is None:
logger.debug("ALB doesn't allow None responses; converting to empty string")
result.body = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,13 @@ def _handle_response(self, *, route: Route, response: Response):
# JSON serialize the body without validation
response.body = jsonable_encoder(response.body, custom_serializer=self._validation_serializer)
else:
# ALB resolver converts None body to "" to prevent ALB 5xx errors,
# but the validation should still see it as None.
response_content = None if response.body == "" and field.type_ in (None, type(None)) else response.body

response.body = self._serialize_response_with_validation(
field=field,
response_content=response.body,
response_content=response_content,
has_route_custom_response_validation=route.custom_response_validation_http_code is not None,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4227,3 +4227,39 @@ def handler(session_id: Annotated[str, Cookie()]):
assert result["statusCode"] == 200
body = json.loads(result["body"])
assert body["session_id"] == "lattice_v1_abc"


def test_alb_response_none_body_with_validation(gw_event_alb):
# GIVEN an ALBResolver with validation enabled
app = ALBResolver(enable_validation=True)

gw_event_alb["path"] = "/no-content"
gw_event_alb["httpMethod"] = "DELETE"

# WHEN a handler returns Response with body=None and return type is None
@app.delete("/no-content")
def handler() -> None:
Comment thread
leandrodamascena marked this conversation as resolved.
return Response(status_code=204, body=None)

# THEN the response should be 204 with empty body (not 422 validation error)
result = app(gw_event_alb, {})
assert result["statusCode"] == 204
assert result["body"] == ""


def test_alb_response_typed_none_body_with_validation(gw_event_alb):
# GIVEN an ALBResolver with validation enabled
app = ALBResolver(enable_validation=True)

gw_event_alb["path"] = "/no-content"
gw_event_alb["httpMethod"] = "DELETE"

# WHEN a handler returns Response[None] with body=None
@app.delete("/no-content")
def handler() -> Response[None]:
return Response(status_code=204, body=None)

# THEN the response should be 204 with empty body (not 422 validation error)
result = app(gw_event_alb, {})
assert result["statusCode"] == 204
assert result["body"] == ""
Loading