Fix: make solve_dependencies re-raise RuntimeError

If an async generator dependency raises RuntimeError:
generator didn't yield, make solve_dependencies catch
and re-raise it, to more easily identify the dependency
responsible for the error, and to provide more information
on how to fix the dependency
This commit is contained in:
Jeremy Epstein 2025-01-28 13:55:51 +11:00
parent 6fae64ff49
commit 8ffd22f890
2 changed files with 48 additions and 2 deletions

View File

@ -54,7 +54,7 @@ from fastapi.concurrency import (
contextmanager_in_threadpool,
)
from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.exceptions import DependencyScopeError
from fastapi.exceptions import DependencyScopeError, FastAPIError
from fastapi.logger import logger
from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes
@ -549,7 +549,24 @@ async def _solve_generator(
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values))
elif dependant.is_async_gen_callable:
cm = asynccontextmanager(dependant.call)(**sub_values)
return await stack.enter_async_context(cm)
try:
solved = await stack.enter_async_context(cm)
except RuntimeError as ex:
if str(ex) != "generator didn't yield":
raise ex
dependency_name = getattr(call, "__name__", "(unknown)")
raise FastAPIError(
f"Dependency {dependency_name} raised: {ex}. There's a high chance that "
"this is a dependency with yield that has a block with a bare except, or a "
"block with except Exception, and is not raising the exception again. Read "
"more about it in the docs: "
"https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield"
"/#dependencies-with-yield-and-except"
) from ex
return solved
@dataclass

View File

@ -0,0 +1,29 @@
import pytest
from anyio import open_file
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
async def get_username():
try:
async with await open_file("/path/to/sanchez.txt", "r") as f:
yield await f.read() # pragma: no cover
except OSError as ex:
raise RuntimeError("File something something, wubba lubba dub dub!") from ex
@app.get("/me")
def get_me(username: str = Depends(get_username)):
return username # pragma: no cover
client = TestClient(app)
@pytest.mark.anyio
def test_runtime_error():
with pytest.raises(RuntimeError) as exc_info:
client.get("/me")
assert "File something something" in exc_info.value.args[0]