Fastapi: [QUESTION] Is it possible to catch exceptions in async generator dependencies?

Created on 21 Oct 2019  路  6Comments  路  Source: tiangolo/fastapi

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 get_db function:

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:

HTTP/1.1 500 Internal Server Error
content-length: 21
content-type: text/plain; charset=utf-8
date: Mon, 21 Oct 2019 21:24:53 GMT
server: uvicorn

Internal Server Error

This is normally possible using asynccontextmanager like so:

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:

before
got hello
got an exception oh no
after

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 馃槃
Fotis

question

All 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.

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.

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:

Finally was executed.
Traceback (most recent call last):
  File "meow.py", line 29, in <module>
    print(TestClient(app).get("/").json())
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/requests/sessions.py", line 546, in get
    return self.request('GET', url, **kwargs)
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/starlette/testclient.py", line 421, in request
    json=json,
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/requests/sessions.py", line 533, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/requests/sessions.py", line 646, in send
    r = adapter.send(request, **kwargs)
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/starlette/testclient.py", line 238, in send
    raise exc from None
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/starlette/testclient.py", line 235, in send
    loop.run_until_complete(self.app(scope, receive, send))
  File "/usr/lib64/python3.6/asyncio/base_events.py", line 484, in run_until_complete
    return future.result()
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/fastapi/applications.py", line 139, in __call__
    await super().__call__(scope, receive, send)
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/async_exit_stack/_async_exit_stack.py", line 208, in __aexit__
    raise exc_details[1]
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/async_exit_stack/_async_exit_stack.py", line 191, in __aexit__
    cb_suppress = await cb(*exc_details)
  File "/home/fots/.virtualenv/testapp/lib64/python3.6/site-packages/async_generator/_util.py", line 53, in __aexit__
    await self._agen.athrow(type, value, traceback)
  File "meow.py", line 18, in error_handling_dependency
    detail="Error was handled.",
fastapi.exceptions.HTTPException

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. 馃槃

Thanks! helped me too.

@fgimian you can't raise exceptions from dependencies _after_ the yield. You can raise before the yield and it will be handled by the default exception handler (even before except 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 the except block. And that would be long after sending the response. Also, long after the last chance to raise an HTTPException, but you could still handle the error and close the DB session.

Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.

Was this page helpful?
0 / 5 - 0 ratings