This commit is contained in:
Jeremy Epstein 2025-12-16 21:07:31 +00:00 committed by GitHub
commit db3fa02cec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 107 additions and 2 deletions

View File

@ -56,7 +56,7 @@ from fastapi.concurrency import (
contextmanager_in_threadpool, contextmanager_in_threadpool,
) )
from fastapi.dependencies.models import Dependant from fastapi.dependencies.models import Dependant
from fastapi.exceptions import DependencyScopeError from fastapi.exceptions import DependencyScopeError, FastAPIError
from fastapi.logger import logger from fastapi.logger import logger
from fastapi.security.oauth2 import SecurityScopes from fastapi.security.oauth2 import SecurityScopes
from fastapi.types import DependencyCacheKey from fastapi.types import DependencyCacheKey
@ -579,7 +579,23 @@ async def _solve_generator(
cm = asynccontextmanager(dependant.call)(**sub_values) cm = asynccontextmanager(dependant.call)(**sub_values)
elif dependant.is_gen_callable: elif dependant.is_gen_callable:
cm = contextmanager_in_threadpool(contextmanager(dependant.call)(**sub_values)) 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 @dataclass

View File

@ -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."
)