Skip to content

Commit 5bc8d00

Browse files
fix(event_handler): resolve dependency injection failure with arbitrary return types (#8334)
fix(event_handler): resolve dependency injection failure with arbitrary return types (#8330)
1 parent a9fce32 commit 5bc8d00

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

aws_lambda_powertools/event_handler/openapi/dependant.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def get_dependant(
156156
call: Callable[..., Any],
157157
name: str | None = None,
158158
responses: dict[int, OpenAPIResponse] | None = None,
159+
is_dependency: bool = False,
159160
) -> Dependant:
160161
"""
161162
Returns a dependant model for a handler function. A dependant model is a model that contains
@@ -200,6 +201,7 @@ def get_dependant(
200201
sub_dependant = get_dependant(
201202
path=path,
202203
call=depends_instance.dependency,
204+
is_dependency=True,
203205
)
204206
dependant.dependencies.append(
205207
DependencyParam(
@@ -229,7 +231,12 @@ def get_dependant(
229231
else:
230232
add_param_to_fields(field=param_field, dependant=dependant)
231233

232-
_add_return_annotation(dependant, endpoint_signature)
234+
# A dependency's return value is injected directly into the handler, never serialized as a
235+
# response body nor validated as an OpenAPI parameter — so building a Pydantic schema for it is
236+
# both unused (no reader consumes a sub-dependency's return_param) and actively harmful: it crashes
237+
# for arbitrary return types (e.g. boto3/botocore clients). Only the top-level handler needs it.
238+
if not is_dependency:
239+
_add_return_annotation(dependant, endpoint_signature)
233240
_add_extra_responses(dependant, responses)
234241

235242
return dependant

tests/functional/event_handler/_pydantic/test_depends.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,82 @@ def handler(name: str = "world", greeting: Annotated[str, Depends(get_greeting)]
214214
result = app(event, {})
215215
assert result["statusCode"] == 200
216216
assert json.loads(result["body"]) == {"message": "hello, Lambda!"}
217+
218+
219+
class ArbitraryClient:
220+
"""Stand-in for an arbitrary non-Pydantic type such as a boto3 client."""
221+
222+
def __init__(self, name: str = "default"):
223+
self.name = name
224+
225+
226+
def test_depends_with_arbitrary_return_type_and_validation():
227+
"""A dependency returning a non-Pydantic type must not crash under enable_validation (#8330)."""
228+
app = APIGatewayHttpResolver(enable_validation=True)
229+
230+
def get_client() -> ArbitraryClient:
231+
return ArbitraryClient(name="orders")
232+
233+
@app.get("/items")
234+
def handler(client: Annotated[ArbitraryClient, Depends(get_client)]):
235+
return {"client": client.name}
236+
237+
event = {**API_GW_V2_EVENT}
238+
event["rawPath"] = "/items"
239+
event["requestContext"] = {
240+
**event["requestContext"],
241+
"http": {"method": "GET", "path": "/items"},
242+
}
243+
244+
result = app(event, {})
245+
assert result["statusCode"] == 200
246+
assert json.loads(result["body"]) == {"client": "orders"}
247+
248+
249+
def test_depends_nested_arbitrary_return_types_and_validation():
250+
"""A nested dependency chain of non-Pydantic return types must resolve (#8330).
251+
252+
Mirrors the reported chain: botocore session -> boto3 session -> dynamodb client.
253+
"""
254+
app = APIGatewayHttpResolver(enable_validation=True)
255+
256+
def get_session() -> ArbitraryClient:
257+
return ArbitraryClient(name="session")
258+
259+
def get_client(session: Annotated[ArbitraryClient, Depends(get_session)]) -> ArbitraryClient:
260+
return ArbitraryClient(name=f"client-of-{session.name}")
261+
262+
@app.get("/items")
263+
def handler(client: Annotated[ArbitraryClient, Depends(get_client)]):
264+
return {"client": client.name}
265+
266+
event = {**API_GW_V2_EVENT}
267+
event["rawPath"] = "/items"
268+
event["requestContext"] = {
269+
**event["requestContext"],
270+
"http": {"method": "GET", "path": "/items"},
271+
}
272+
273+
result = app(event, {})
274+
assert result["statusCode"] == 200
275+
assert json.loads(result["body"]) == {"client": "client-of-session"}
276+
277+
278+
def test_depends_arbitrary_return_type_excluded_from_openapi_schema():
279+
"""A non-Pydantic dependency return type must not appear in (or break) the OpenAPI schema (#8330)."""
280+
app = APIGatewayHttpResolver(enable_validation=True)
281+
282+
def get_client() -> ArbitraryClient:
283+
return ArbitraryClient()
284+
285+
@app.get("/items")
286+
def handler(client: Annotated[ArbitraryClient, Depends(get_client)]):
287+
return {"ok": True}
288+
289+
# Schema generation itself must not raise, and the dependency must not leak into params/body.
290+
schema = app.get_openapi_schema()
291+
get_op = schema.paths["/items"].get
292+
param_names = [p.name for p in (get_op.parameters or [])]
293+
294+
assert "client" not in param_names
295+
assert get_op.requestBody is None

0 commit comments

Comments
 (0)