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

Labels

Type

No type

Projects

Status

Working on it

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions