diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index cc7e55b4b..698b28281 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -56,7 +56,7 @@ from fastapi.concurrency import ( contextmanager_in_threadpool, ) from fastapi.dependencies.models import Dependant -from fastapi.exceptions import DependencyScopeError +from fastapi.exceptions import DependencyScopeError, FastAPIError from fastapi.logger import logger from fastapi.security.oauth2 import SecurityScopes from fastapi.types import DependencyCacheKey @@ -579,7 +579,23 @@ async def _solve_generator( cm = asynccontextmanager(dependant.call)(**sub_values) elif dependant.is_gen_callable: cm = contextmanager_in_threadpool(contextmanager(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(dependant.call, "__name__", "(unknown)") + raise FastAPIError( + f"Dependency {dependency_name} raised: {ex}. There's a high chance that " + "this is a dependency with yield that catches an exception using except, " + "but doesn't raise 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 diff --git a/tests/test_dependency_runtime_errors.py b/tests/test_dependency_runtime_errors.py new file mode 100644 index 000000000..f091e7cda --- /dev/null +++ b/tests/test_dependency_runtime_errors.py @@ -0,0 +1,89 @@ +import pytest +from anyio import open_file +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +def get_username_reraises(): + try: + with open("/nonexistent/path.txt") as f: + yield f.read() # pragma: no cover + except OSError as ex: + raise RuntimeError("File read error") from ex + + +def get_username_doesnt_reraise(): + try: + with open("/nonexistent/path.txt") as f: + yield f.read() # pragma: no cover + except OSError: + print("Didn't re-raise") + + +async def get_username_reraises_async(): + try: + async with await open_file("/nonexistent/path.txt", "r") as f: + yield await f.read() # pragma: no cover + except OSError as ex: + raise RuntimeError("File read error") from ex + + +async def get_username_doesnt_reraise_async(): + try: + async with await open_file("/nonexistent/path.txt", "r") as f: + yield await f.read() # pragma: no cover + except OSError: + print("Didn't re-raise") + + +@app.get("/reraises") +def get_me_reraises(username: str = Depends(get_username_reraises)): + return username # pragma: no cover + + +@app.get("/doesnt-reraise") +def get_me_doesnt_reraise(username: str = Depends(get_username_doesnt_reraise)): + return username # pragma: no cover + + +@app.get("/reraises-async") +def get_me_reraises_async(username: str = Depends(get_username_reraises_async)): + return username # pragma: no cover + + +@app.get("/doesnt-reraise-async") +def get_me_doesnt_reraise_async( + username: str = Depends(get_username_doesnt_reraise_async), +): + return username # pragma: no cover + + +client = TestClient(app) + + +@pytest.mark.anyio +@pytest.mark.parametrize("path", ["/reraises", "/reraises-async"]) +def test_runtime_error_reraises(path: str): + with pytest.raises(RuntimeError) as exc_info: + client.get(path) + assert str(exc_info.value) == "File read error" + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("path", "fn_name"), + [ + ("/doesnt-reraise", "get_username_doesnt_reraise"), + ("/doesnt-reraise-async", "get_username_doesnt_reraise_async"), + ], +) +def test_runtime_error_doesnt_reraise(path: str, fn_name: str): + with pytest.raises(RuntimeError) as exc_info: + client.get(path) + assert str(exc_info.value).startswith( + f"Dependency {fn_name} raised: generator didn't yield. " + "There's a high chance that this is a dependency with yield that catches an " + "exception using except, but doesn't raise the exception again." + )