-
Description Is it possible for an async generator dependency (as seen in the docs) to handle exceptions thrown by an endpoint? Additional context Suppose I have the following async def get_db(request: Request) -> asyncpg.connection.Connection:
"""Obtain a database connection from the pool."""
if pool is None:
logger.error("Unable to provide a connection on an uninitalised pool.")
raise HTTPException(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unable to provide a connection on an uninitalised pool.",
)
conn = None
try:
conn = await pool.acquire()
# Test that the connection is still active by running a trivial query
# (https://docs.sqlalchemy.org/en/13/core/pooling.html#disconnect-handling-pessimistic)
try:
await conn.execute("SELECT 1")
except ConnectionDoesNotExistError:
conn = await pool.acquire()
yield conn
except (PostgresConnectionError, OSError) as e:
logger.error("Unable to connect to the database: %s", e)
raise HTTPException(
status_code=HTTP_500_INTERNAL_SERVER_ERROR, detail="Unable to connect to the database."
)
# --- The problem is with this particular exception handling ---
except SyntaxOrAccessError as e:
logger.error("Unable to execute query: %s", e)
raise HTTPException(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unable to execute the required query against the database.",
)
finally:
if pool is not None and conn:
await pool.release(conn) If an endpoint using this dependency runs an invalid query, I would like to catch that exception and handle it here. However, it seems that due to the way FastAPI is wrapping the async context manager, this is not possible. Instead the exception is not caught by my dependency function, but instead thrown in the ASGI console. The client just sees:
This is normally possible using from async_generator import asynccontextmanager
@asynccontextmanager
async def boo():
print("before")
try:
yield "hello"
except ValueError as e:
print("got an exception", e)
print("after")
async with boo() as b:
print("got", b)
raise ValueError("oh no") Output:
I suspect this relates to the following function and how it handles exceptions: https://github.com/tiangolo/fastapi/blob/master/fastapi/concurrency.py#L36 Thanks so much for your help in advance 😄 |
Beta Was this translation helpful? Give feedback.
Replies: 6 comments
-
@fgimian if it isn't behaving the way you want, maybe you can create a reproducible example? It seems to work as intended for me: import logging
from fastapi import Depends, FastAPI, HTTPException
from starlette.requests import Request
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
from starlette.testclient import TestClient
app = FastAPI()
logger = logging.getLogger(__name__)
async def error_handling_dependency(request: Request) -> None:
try:
raise ValueError("Got an error")
yield
except ValueError:
raise HTTPException(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error was handled.",
)
finally:
logger.error("Finally was executed.")
@app.get("/")
async def get(x: None = Depends(error_handling_dependency)):
return x
print(TestClient(app).get("/").json())
# Finally was executed.
# {'detail': 'Error was handled.'} I think it should be able to handle exceptions. |
Beta Was this translation helpful? Give feedback.
-
Oh, I see what you mean, you want it to basically work as an exception handler. I think that might be hard to handle properly. The reason for that is that the dependency function isn't executed in a parent scope of the endpoint function. But it should at least currently behave properly in terms of cleanup. I suspect there may be a way to make this work as is or with minor modifications to fastapi, but I think it might require some heavy digging into contextlib to figure it out. |
Beta Was this translation helpful? Give feedback.
-
Thanks so much @dmontagu, yep that's correct. Here's an example using a modified version of your code above: import logging
from fastapi import Depends, FastAPI, HTTPException
from starlette.requests import Request
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
from starlette.testclient import TestClient
app = FastAPI()
logger = logging.getLogger(__name__)
async def error_handling_dependency(request: Request) -> None:
try:
yield "boo"
except ValueError:
raise HTTPException(
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error was handled.",
)
finally:
logger.error("Finally was executed.")
@app.get("/")
async def get(x: None = Depends(error_handling_dependency)):
raise ValueError("oh no")
print(TestClient(app).get("/").json()) Output:
Yeah, I'm not sure if there's a way to make this work technically with the threadpool that FastAPI is using. But it would be lovely if it did. 😄 |
Beta Was this translation helpful? Give feedback.
-
Thanks! helped me too. |
Beta Was this translation helpful? Give feedback.
-
@fgimian you can't raise exceptions from dependencies after the But the context of dependencies is before/around the context of exception handlers. It happens outside of the request handling. So, you could even return a response, and then have a bunch of background tasks running using the same DB session from the dependency, and they could throw an error, and it would still be caught by the dependency with |
Beta Was this translation helpful? Give feedback.
-
Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues. |
Beta Was this translation helpful? Give feedback.
@fgimian you can't raise exceptions from dependencies after the
yield
. You can raise before theyield
and it will be handled by the default exception handler (even beforeexcept
code in your dependency is reached).But the context of dependencies is before/around the context of exception handlers. It happens outside of the request handling.
So, you could even return a response, and then have a bunch of background tasks running using the same DB session from the dependency, and they could throw an error, and it would still be caught by the dependency with
yield
in the theexcept
block. And that would be long after sending the response. Also, long after the last chance to raise anHTTPExcep…