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
9 changes: 8 additions & 1 deletion aws_lambda_powertools/event_handler/openapi/dependant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -200,6 +201,7 @@ def get_dependant(
sub_dependant = get_dependant(
path=path,
call=depends_instance.dependency,
is_dependency=True,
)
dependant.dependencies.append(
DependencyParam(
Expand Down Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions tests/functional/event_handler/_pydantic/test_depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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