Skip to content

Bug: Forward References in Annotated Type Hints Cause Body Parameters to be Incorrectly Treated as Query Parameters in Event Handler #5884

Closed
@xdxindustries

Description

@xdxindustries

Expected Behaviour

When using quoted type annotations with Annotated in AWS Lambda Powertools API Gateway resolver, the parameter should be correctly identified as a body parameter and properly parse the incoming JSON request body.

When using quoted type annotations (forward references) like 'Annotated[UserCreate, Body(...)]', the parameter is incorrectly treated as a query parameter instead of a body parameter. This results in a 422 Unprocessable Content error with a "missing query parameter" message, even when the JSON body is correctly provided.

Current Behaviour

  • Type resolution fails silently
  • Parameter placement falls back to "best guess" behavior
  • Complex types may be misclassified as simple types
  • Explicit parameter annotations are ignored

Code snippet

import json
from typing import Annotated

from aws_lambda_powertools import Tracer
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.openapi.params import Body
from pydantic import BaseModel

app = APIGatewayRestResolver(enable_validation=True)

class UserCreate(BaseModel):
    username: str
    email: str
    age: int
    id: str

@app.post("/users")
def create_user(
    user_create_request: 'Annotated[UserCreate, Body(title="New User", description="The user data to create")]',
) -> dict:
    return {"message": f"Created user {user_create_request.username}"}

def lambda_handler(event, context):
    return app.resolve(event, context)

if __name__ == "__main__":
    test_event = {
        "body": json.dumps({
            "username": "john_doe",
            "email": "[email protected]",
            "age": 30,
            "id": "user123"
        }),
        "httpMethod": "POST",
        "path": "/users",
        "headers": {
            "Content-Type": "application/json"
        },
        "requestContext": {
            "requestId": "test-id"
        }
    }
    
    result = lambda_handler(test_event, {})
    if (
        result["statusCode"] == 422 
        and "missing" in result["body"]
        and "user_create_request" in result["body"]
    ):
        print("Bug reproduced! Got expected 422 error with missing user_create_request:")
        print(result["body"])
    else:
        print("Unexpected result:")
        print(f"Status: {result['statusCode']}")
        print(f"Body: {result['body']}")

"""
The issue occurs due to how AWS Lambda Powertools handles type resolution:

1. In get_typed_signature(), there's a bug in how it gets the globals for the call:
   
   globalns = getattr(call, "__global__", {})  # Bug: Returns empty dict if __global__ not found
   and that is always true becuase it's not __global_ its __globals__
   
   This fails to get the actual module globals where UserCreate is defined.

2. When evaluating the forward reference string in get_typed_annotation():
   
   annotation = ForwardRef(annotation)
   annotation = evaluate_forwardref(annotation, globalns, globalns)
   
   Because globalns is empty, it can't resolve "UserCreate" or other types in the string.

3. This causes the unresolved ForwardRef to be passed to field_annotation_is_complex(),
   where get_origin() returns None since it can't get the origin of an unresolved reference.

4. With origin=None, field_annotation_is_complex() returns False, making
   field_annotation_is_scalar() return True.

5. Because the parameter is incorrectly identified as a scalar field,
   is_body_param() returns False and the parameter isn't added to
   dependant.body_params.

6. This results in the parameter being treated as a query parameter
   instead of a body parameter, leading to the missing argument error.

The fix could be either:
a) Use direct type annotation instead of a string forward reference
b) Fix get_typed_signature() to properly get the module globals:
   
   globalns = getattr(call, "__globals__", {})  # Use current globals as fallback
   
"""

Possible Solution

Fix get_typed_signature() to properly get the module globals:

globalns = getattr(call, "__globals__", {})

Steps to Reproduce

Steps to Reproduce

  1. Create an API Gateway resolver with validation enabled
  2. Define a Pydantic model for request validation
  3. Create an endpoint using a quoted type annotation with Annotated and Body
  4. Send a POST request with a valid JSON body
  5. Observe the 422 error indicating a missing query parameter

Powertools for AWS Lambda (Python) version

latest

AWS Lambda function runtime

3.8

Packaging format used

Lambda Layers

Debugging logs

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

Status

Shipped

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions