From 2147ccc696622330230c1ab82cef336e236ab374 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Sun, 12 Jul 2026 10:46:11 +0100 Subject: [PATCH] fix(event_handler): resolve dependency injection failure with arbitrary return types (#8330) --- .../event_handler/openapi/dependant.py | 9 ++- .../event_handler/_pydantic/test_depends.py | 79 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/openapi/dependant.py b/aws_lambda_powertools/event_handler/openapi/dependant.py index 1e7f4327602..17d98914c73 100644 --- a/aws_lambda_powertools/event_handler/openapi/dependant.py +++ b/aws_lambda_powertools/event_handler/openapi/dependant.py @@ -156,6 +156,7 @@ def get_dependant( call: Callable[..., Any], name: str | None = None, responses: dict[int, OpenAPIResponse] | None = None, + is_dependency: bool = False, ) -> Dependant: """ Returns a dependant model for a handler function. A dependant model is a model that contains @@ -200,6 +201,7 @@ def get_dependant( sub_dependant = get_dependant( path=path, call=depends_instance.dependency, + is_dependency=True, ) dependant.dependencies.append( DependencyParam( @@ -229,7 +231,12 @@ def get_dependant( else: add_param_to_fields(field=param_field, dependant=dependant) - _add_return_annotation(dependant, endpoint_signature) + # A dependency's return value is injected directly into the handler, never serialized as a + # response body nor validated as an OpenAPI parameter — so building a Pydantic schema for it is + # both unused (no reader consumes a sub-dependency's return_param) and actively harmful: it crashes + # for arbitrary return types (e.g. boto3/botocore clients). Only the top-level handler needs it. + if not is_dependency: + _add_return_annotation(dependant, endpoint_signature) _add_extra_responses(dependant, responses) return dependant diff --git a/tests/functional/event_handler/_pydantic/test_depends.py b/tests/functional/event_handler/_pydantic/test_depends.py index 0cbc05b83c7..1a4be667377 100644 --- a/tests/functional/event_handler/_pydantic/test_depends.py +++ b/tests/functional/event_handler/_pydantic/test_depends.py @@ -214,3 +214,82 @@ def handler(name: str = "world", greeting: Annotated[str, Depends(get_greeting)] result = app(event, {}) assert result["statusCode"] == 200 assert json.loads(result["body"]) == {"message": "hello, Lambda!"} + + +class ArbitraryClient: + """Stand-in for an arbitrary non-Pydantic type such as a boto3 client.""" + + def __init__(self, name: str = "default"): + self.name = name + + +def test_depends_with_arbitrary_return_type_and_validation(): + """A dependency returning a non-Pydantic type must not crash under enable_validation (#8330).""" + app = APIGatewayHttpResolver(enable_validation=True) + + def get_client() -> ArbitraryClient: + return ArbitraryClient(name="orders") + + @app.get("/items") + def handler(client: Annotated[ArbitraryClient, Depends(get_client)]): + return {"client": client.name} + + event = {**API_GW_V2_EVENT} + event["rawPath"] = "/items" + event["requestContext"] = { + **event["requestContext"], + "http": {"method": "GET", "path": "/items"}, + } + + result = app(event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"client": "orders"} + + +def test_depends_nested_arbitrary_return_types_and_validation(): + """A nested dependency chain of non-Pydantic return types must resolve (#8330). + + Mirrors the reported chain: botocore session -> boto3 session -> dynamodb client. + """ + app = APIGatewayHttpResolver(enable_validation=True) + + def get_session() -> ArbitraryClient: + return ArbitraryClient(name="session") + + def get_client(session: Annotated[ArbitraryClient, Depends(get_session)]) -> ArbitraryClient: + return ArbitraryClient(name=f"client-of-{session.name}") + + @app.get("/items") + def handler(client: Annotated[ArbitraryClient, Depends(get_client)]): + return {"client": client.name} + + event = {**API_GW_V2_EVENT} + event["rawPath"] = "/items" + event["requestContext"] = { + **event["requestContext"], + "http": {"method": "GET", "path": "/items"}, + } + + result = app(event, {}) + assert result["statusCode"] == 200 + assert json.loads(result["body"]) == {"client": "client-of-session"} + + +def test_depends_arbitrary_return_type_excluded_from_openapi_schema(): + """A non-Pydantic dependency return type must not appear in (or break) the OpenAPI schema (#8330).""" + app = APIGatewayHttpResolver(enable_validation=True) + + def get_client() -> ArbitraryClient: + return ArbitraryClient() + + @app.get("/items") + def handler(client: Annotated[ArbitraryClient, Depends(get_client)]): + return {"ok": True} + + # Schema generation itself must not raise, and the dependency must not leak into params/body. + schema = app.get_openapi_schema() + get_op = schema.paths["/items"].get + param_names = [p.name for p in (get_op.parameters or [])] + + assert "client" not in param_names + assert get_op.requestBody is None