Skip to content

Bug: Inconsistent validation of empty responses between ALBResolver and APIGatewayHttpResolver #8186

@chriselion

Description

@chriselion

Expected Behaviour

I'm trying to update to a newer version of aws-lambda-powertools, and I'm hitting problems for endpoints that return "empty" 204 responses (e.g. for a DELETE endpoint). For example:

def delete_endpoint() -> Response[None]:
    return Response(
        status_code=204,
        body=None,
    )

seems to follow the recommendations for the "Using Response with data validation?" section in the docs here.

I would expect both ALBResolver and APIGatewayHttpResolver to treat this the same, and they agree in aws-lambda-powertools==3.25.0 but not in aws-lambda-powertools==3.26.0 and higher.

My suspicion is that this modification of the response body is the cause:

# NOTE: Minor override for early return on Response with null body for ALB
if isinstance(result, Response) and result.body is None:
logger.debug("ALB doesn't allow None responses; converting to empty string")
result.body = ""

Current Behaviour

The ALBResolver returns a response like

{
  "statusCode": 422,
  "body": "{\"statusCode\":422,\"detail\":[{\"loc\":[\"response\"],\"type\":\"none_required\"}]}",
  "isBase64Encoded": false,
  "headers": {
    "Content-Type": "application/json"
  }
}

Code snippet

# /// script
# dependencies = [
#   "pydantic==2.12.0",
#   "aws-lambda-powertools==3.28.0",
# ]
# ///
import json
from typing import Any
from aws_lambda_powertools.event_handler import ALBResolver, APIGatewayHttpResolver, Response


def build_alb_event(*, path: str, method: str) -> dict[str, Any]:
    event: dict[str, Any] = {
        "httpMethod": method,
        "path": path,
        "requestId": "some_request_id",
        "requestContext": {"elb": {"targetGroupArn": ":target:"}},
    }
    return event


def build_api_gateway_event(*, path: str, method: str) -> dict[str, Any]:
    event: dict[str, Any] = {
        "rawPath": path,
        "requestContext": {
            "requestContext": {"requestId": "some_request_id"},
            "http": {"method": method},
            "stage": "$default",
        },
    }

    return event

alb_app = ALBResolver(enable_validation=True)
api_gateway_app = APIGatewayHttpResolver(enable_validation=True)

# I tried multilple return types for the endpoint
# - No return type: OK
# - typing.Any: OK
# - None: 422 from ALB
# - Response[None]: 422 from ALB
# - Response[str]: 422 from API Gateway
# - Response[str] with body="": OK

def delete_endpoint() -> Response[None]:
    return Response(
        status_code=204,
        body=None,
    )

alb_app.route("test", "DELETE")(delete_endpoint)
api_gateway_app.route("test", "DELETE")(delete_endpoint)

alb_resp = alb_app.resolve( build_alb_event(path="test", method="DELETE"), context={} )
api_resp = api_gateway_app.resolve( build_api_gateway_event(path="test", method="DELETE"), context={})

print(f"alb: {json.dumps(alb_resp, indent=2)}")
print(f"api: {json.dumps(api_resp, indent=2)}")

Possible Solution

As noted in the snippet, I tried a few different return type annotations on the function. Omitting the return type, or using typing.Any seems to fix (or at least hide) the problem. The only fix that maintains typing while fixing both ALB and APIGateway behavior seems to returning an empty string instead of None for the Response body, and using Response[str] as the type annotation.

Steps to Reproduce

Run the attached script. I used uv run repro_204.py, but installing with pip should also work.

Powertools for AWS Lambda (Python) version

3.26.0 and higher

AWS Lambda function runtime

3.12

Packaging format used

Lambda Layers

Debugging logs

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriagePending triage from maintainers

    Type

    No type

    Projects

    Status

    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions