mirror of https://github.com/tiangolo/fastapi.git
Merge c3eca6b05c into 272204c0c7
This commit is contained in:
commit
db3fa02cec
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue