Skip to content
Open
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
18 changes: 17 additions & 1 deletion fastapi/dependencies/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import builtins
import dataclasses
import inspect
import sys
Expand Down Expand Up @@ -242,10 +243,25 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
return typed_signature


class _LenientTypeResolutionDict(dict[str, Any]):
def __missing__(self, key: str) -> Any:
return vars(builtins).get(key, ForwardRef(key))


def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
if isinstance(annotation, str):
annotation = ForwardRef(annotation)
annotation = evaluate_forwardref(annotation, globalns, globalns) # ty: ignore[deprecated]
if isinstance(annotation, ForwardRef):
annotation = evaluate_forwardref( # ty: ignore[deprecated]
annotation, globalns, globalns
)
if isinstance(annotation, ForwardRef):
try:
annotation = eval( # noqa: S307
annotation.__forward_arg__, _LenientTypeResolutionDict(globalns)
)
except Exception:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bare except Exception: pass silently swallows all eval errors

The fallback eval() catches and discards all exceptions including SyntaxError, NameError, and TypeError. When a developer has a genuine typo or syntax error in a complex Annotated type annotation, this will silently degrade to an unresolved ForwardRef with no diagnostic output, making the root cause very hard to track down. Consider logging a debug/warning message or narrowing the exception type.

Add a logger.debug() or warnings.warn() inside the except block so developers can diagnose annotation resolution failures, e.g. except Exception as e: logger.debug(f'Failed to resolve ForwardRef {annotation.__forward_arg__!r}: {e}').

pass
if annotation is type(None):
return None
return annotation
Expand Down
20 changes: 20 additions & 0 deletions tests/test_dependencies_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Annotated, ForwardRef, get_args, get_origin

from fastapi import Depends, params
from fastapi.dependencies.utils import get_typed_annotation


Expand All @@ -6,3 +9,20 @@ def test_get_typed_annotation():
annotation = "None"
typed_annotation = get_typed_annotation(annotation, globals())
assert typed_annotation is None


def test_get_typed_annotation_falls_back_to_lenient_forwardref_resolution():
def dependency() -> None:
return None

annotation = "Annotated[Potato, Depends(dependency)]"
typed_annotation = get_typed_annotation(
annotation,
{"Annotated": Annotated, "Depends": Depends, "dependency": dependency},
)

assert get_origin(typed_annotation) is Annotated
type_annotation, depends = get_args(typed_annotation)
assert isinstance(type_annotation, ForwardRef)
assert type_annotation.__forward_arg__ == "Potato"
assert isinstance(depends, params.Depends)
59 changes: 59 additions & 0 deletions tests/test_stringified_annotation_forwardref_dependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from inline_snapshot import snapshot

app = FastAPI()


def get_potato() -> Potato:
return Potato(color="red", size=10)


@app.get("/")
async def read_root(potato: Annotated[Potato, Depends(get_potato)]):
return {"color": potato.color, "size": potato.size}


@dataclass
class Potato:
color: str
size: int


client = TestClient(app)


def test_stringified_annotated_forwardref_dependency():
response = client.get("/")
assert response.status_code == 200, response.text
assert response.json() == {"color": "red", "size": 10}


def test_stringified_annotated_forwardref_dependency_openapi():
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
assert response.json() == snapshot(
{
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/": {
"get": {
"summary": "Read Root",
"operationId": "read_root__get",
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
}
}
},
}
)