Browse Source

Merge efe0e06f8d into 1d434dec47

pull/13264/merge
Jeremy Epstein 4 days ago
committed by GitHub
parent
commit
dc70963372
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      docs/en/docs/tutorial/dependencies/dependencies-with-yield.md
  2. 17
      docs_src/dependencies/tutorial008e.py
  3. 18
      docs_src/dependencies/tutorial008e_an.py
  4. 19
      docs_src/dependencies/tutorial008e_an_py39.py
  5. 18
      docs_src/dependencies/tutorial008f.py
  6. 19
      docs_src/dependencies/tutorial008f_an.py
  7. 20
      docs_src/dependencies/tutorial008f_an_py39.py
  8. 20
      fastapi/dependencies/utils.py
  9. 29
      tests/test_dependency_runtime_errors.py
  10. 41
      tests/test_tutorial/test_dependencies/test_tutorial008e.py
  11. 37
      tests/test_tutorial/test_dependencies/test_tutorial008f.py

10
docs/en/docs/tutorial/dependencies/dependencies-with-yield.md

@ -117,6 +117,10 @@ If you catch an exception using `except` in a dependency with `yield` and you do
{* ../../docs_src/dependencies/tutorial008c_an_py39.py hl[15:16] *}
The same applies for an `async` dependency with `yield`:
{* ../../docs_src/dependencies/tutorial008e_an_py39.py hl[13:14] *}
In this case, the client will see an *HTTP 500 Internal Server Error* response as it should, given that we are not raising an `HTTPException` or similar, but the server will **not have any logs** or any other indication of what was the error. 😱
### Always `raise` in Dependencies with `yield` and `except`
@ -127,7 +131,11 @@ You can re-raise the same exception using `raise`:
{* ../../docs_src/dependencies/tutorial008d_an_py39.py hl[17] *}
Now the client will get the same *HTTP 500 Internal Server Error* response, but the server will have our custom `InternalError` in the logs. 😎
The same applies for an `async` dependency with `yield`:
{* ../../docs_src/dependencies/tutorial008f_an_py39.py hl[15] *}
Now the client will get the same *HTTP 500 Internal Server Error* response, but the server will have our custom `InternalError` or `OSError` in the logs. 😎
## Execution of dependencies with `yield`

17
docs_src/dependencies/tutorial008e.py

@ -0,0 +1,17 @@
from anyio import open_file
from fastapi import Depends, FastAPI
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:
print("We didn't re-raise, wubba lubba dub dub!")
@app.get("/me")
def get_me(username: str = Depends(get_username)):
return username # pragma: no cover

18
docs_src/dependencies/tutorial008e_an.py

@ -0,0 +1,18 @@
from anyio import open_file
from fastapi import Depends, FastAPI
from typing_extensions import Annotated
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:
print("We didn't re-raise, wubba lubba dub dub!")
@app.get("/me")
def get_me(username: Annotated[str, Depends(get_username)]):
return username # pragma: no cover

19
docs_src/dependencies/tutorial008e_an_py39.py

@ -0,0 +1,19 @@
from typing import Annotated
from anyio import open_file
from fastapi import Depends, FastAPI
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:
print("We didn't re-raise, wubba lubba dub dub!")
@app.get("/me")
def get_me(username: Annotated[str, Depends(get_username)]):
return username # pragma: no cover

18
docs_src/dependencies/tutorial008f.py

@ -0,0 +1,18 @@
from anyio import open_file
from fastapi import Depends, FastAPI
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:
print("We don't swallow the OS error here, we raise again 😎")
raise
@app.get("/me")
def get_me(username: str = Depends(get_username)):
return username # pragma: no cover

19
docs_src/dependencies/tutorial008f_an.py

@ -0,0 +1,19 @@
from anyio import open_file
from fastapi import Depends, FastAPI
from typing_extensions import Annotated
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:
print("We don't swallow the OS error here, we raise again 😎")
raise
@app.get("/me")
def get_me(username: Annotated[str, Depends(get_username)]):
return username # pragma: no cover

20
docs_src/dependencies/tutorial008f_an_py39.py

@ -0,0 +1,20 @@
from typing import Annotated
from anyio import open_file
from fastapi import Depends, FastAPI
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:
print("We don't swallow the OS error here, we raise again 😎")
raise
@app.get("/me")
def get_me(username: Annotated[str, Depends(get_username)]):
return username # pragma: no cover

20
fastapi/dependencies/utils.py

@ -52,6 +52,7 @@ from fastapi.concurrency import (
contextmanager_in_threadpool,
)
from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.exceptions import FastAPIError
from fastapi.logger import logger
from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes
@ -557,7 +558,24 @@ async def solve_generator(
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
elif is_async_gen_callable(call):
cm = asynccontextmanager(call)(**sub_values)
return await stack.enter_async_context(cm)
try:
solved = await stack.enter_async_context(cm)
except RuntimeError as ex:
if "generator didn't yield" not in f"{ex}":
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

29
tests/test_dependency_runtime_errors.py

@ -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]

41
tests/test_tutorial/test_dependencies/test_tutorial008e.py

@ -0,0 +1,41 @@
import importlib
from types import ModuleType
import pytest
from fastapi.exceptions import FastAPIError
from fastapi.testclient import TestClient
from ...utils import needs_py39
@pytest.fixture(
name="mod",
params=[
"tutorial008e",
"tutorial008e_an",
pytest.param("tutorial008e_an_py39", marks=needs_py39),
],
)
def get_mod(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.dependencies.{request.param}")
return mod
@pytest.mark.anyio
def test_fastapi_error(mod: ModuleType):
client = TestClient(mod.app)
with pytest.raises(FastAPIError) as exc_info:
client.get("/me")
assert (
"Dependency get_username raised: generator didn't yield"
in exc_info.value.args[0]
)
@pytest.mark.anyio
def test_internal_server_error(mod: ModuleType):
client = TestClient(mod.app, raise_server_exceptions=False)
response = client.get("/me")
assert response.status_code == 500, response.text
assert response.text == "Internal Server Error"

37
tests/test_tutorial/test_dependencies/test_tutorial008f.py

@ -0,0 +1,37 @@
import importlib
from types import ModuleType
import pytest
from fastapi.testclient import TestClient
from ...utils import needs_py39
@pytest.fixture(
name="mod",
params=[
"tutorial008f",
"tutorial008f_an",
pytest.param("tutorial008f_an_py39", marks=needs_py39),
],
)
def get_mod(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.dependencies.{request.param}")
return mod
@pytest.mark.anyio
def test_os_error(mod: ModuleType):
client = TestClient(mod.app)
with pytest.raises(OSError) as exc_info:
client.get("/me")
assert "No such file or directory" in str(exc_info.value)
@pytest.mark.anyio
def test_internal_server_error(mod: ModuleType):
client = TestClient(mod.app, raise_server_exceptions=False)
response = client.get("/me")
assert response.status_code == 500, response.text
assert response.text == "Internal Server Error"
Loading…
Cancel
Save