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] *} {* ../../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. 😱 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` ### 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] *} {* ../../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` ## 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, contextmanager_in_threadpool,
) )
from fastapi.dependencies.models import Dependant, SecurityRequirement from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.exceptions import FastAPIError
from fastapi.logger import logger from fastapi.logger import logger
from fastapi.security.base import SecurityBase from fastapi.security.base import SecurityBase
from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.oauth2 import OAuth2, SecurityScopes
@ -557,7 +558,24 @@ async def solve_generator(
cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values)) cm = contextmanager_in_threadpool(contextmanager(call)(**sub_values))
elif is_async_gen_callable(call): elif is_async_gen_callable(call):
cm = asynccontextmanager(call)(**sub_values) 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 @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