Catch `Exception` globally in FastAPI
Asked Answered
P

8

46

I am trying to catch unhandled exceptions at global level. So somewhere in main.py file I have the below:

@app.exception_handler(Exception)
async def exception_callback(request: Request, exc: Exception):
  logger.error(exc.detail)

But the above method is never executed. However, if I write a custom exception and try to catch it (as shown below), it works just fine.

class MyException(Exception):
  #some code

@app.exception_handler(MyException)
async def exception_callback(request: Request, exc: MyException):
  logger.error(exc.detail)

I have gone through Catch exception type of Exception and process body request #575. But this bug talks about accessing request body. After seeing this bug, I feel it should be possible to catch Exception. FastAPI version I am using is: fastapi>=0.52.0.

Thanks in advance :)


Update

There are multiple answers, I am thankful to all the readers and authors here. I was revisiting this solution in my application. Now I see that I needed to set debug=False, default it's False, but I had it set to True in

server = FastAPI(
    title=app_settings.PROJECT_NAME,
    version=app_settings.VERSION,
)

It seems that I missed it when @iedmrc commented on answer given by @Kavindu Dodanduwa.

Panpipe answered 4/5, 2020 at 16:23 Comment(3)
Ajeet I must say that I cannot reproduce your problems using fastapi[all]==0.65.1 and starlette==0.14.2 . I have a project with the exact setup as you describe except that I have an additional return JSONResponse(status_code=500, content={"message": "internal server error"}) in exception_callback.Thekla
Related answers can be found here and here, as well as here and hereDonative
Future readers might find this answer helpful as well, which demonstrates how to catch any Exception, including Starlette's HTTPException.Donative
I
46

In case you want to capture all unhandled exceptions (internal server error), there's a very simple way of doing it. Documentation

from fastapi import FastAPI
from starlette.requests import Request
from starlette.responses import Response
from traceback import print_exception

app = FastAPI()

async def catch_exceptions_middleware(request: Request, call_next):
    try:
        return await call_next(request)
    except Exception:
        # you probably want some kind of logging here
        print_exception(e)
        return Response("Internal server error", status_code=500)

app.middleware('http')(catch_exceptions_middleware)

Make sure you place this middleware before everything else.

Intervention answered 16/6, 2020 at 11:11 Comment(3)
This one awaits for all task to finish. Thus, it prevents to schedule a task to run in background.Lupus
the 'middleware' example doesn't work for me but the usage of 'route' in the official documentation works like a charm fastapi.tiangolo.com/advanced/custom-request-and-route/…Reservation
from Starlette 0.21.0 it's possible to to use BackgroundTasks with BaseHTTPMiddleware see github.com/laurentS/slowapi/issues/98#issuecomment-1259826739Suds
T
16

You can do something like this. It should return a json object with your custom error message also works in debugger mode.

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.exception_handler(Exception)
async def validation_exception_handler(request: Request, exc: Exception):
    # Change here to Logger
    return JSONResponse(
        status_code=500,
        content={
            "message": (
                f"Failed method {request.method} at URL {request.url}."
                f" Exception message is {exc!r}."
            )
        },
    )
Trammell answered 17/1, 2021 at 13:5 Comment(3)
This is a great solution that actually worked for me.Raja
As mentioned above this still produces an "Exception in ASGI application_"Donative
@Donative Now it must be fixed since version 0.15. I did not test it yet.Barimah
K
9

Adding a custom APIRoute can be also be used to handle global exceptions. The advantage of this approach is that if a http exception is raised from the custom route it will be handled by default Starlette's error handlers:

from typing import Callable

from fastapi import Request, Response, HTTPException, APIRouter, FastAPI
from fastapi.routing import APIRoute
from .logging import logger


class RouteErrorHandler(APIRoute):
    """Custom APIRoute that handles application errors and exceptions"""

    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            try:
                return await original_route_handler(request)
            except Exception as ex:
                if isinstance(ex, HTTPException):
                    raise ex
                logger.exception("uncaught error")
                # wrap error into pretty 500 exception
                raise HTTPException(status_code=500, detail=str(ex))

        return custom_route_handler


router = APIRouter(route_class=RouteErrorHandler)

app = FastAPI()
app.include_router(router)

Worked for me with fastapi==0.68.1.

More on custom routes: https://fastapi.tiangolo.com/advanced/custom-request-and-route/

Khamsin answered 26/10, 2021 at 9:54 Comment(1)
Note that @app.exception_handlers appear to be invoked after the route handler is called. You will need to check for and raise any other exception type that you have define an app exception handler for, e.g. RequestValidationError.Dekow
P
6

It is a known issue on the Fastapi and Starlette.

I am trying to capture the StarletteHTTPException globally by a following simple sample.

import uvicorn

from fastapi import FastAPI
from starlette.requests import Request
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse

app = FastAPI()


@app.exception_handler(StarletteHTTPException)
async def exception_callback(request: Request, exc: Exception):
    print("test")
    return JSONResponse({"detail": "test_error"}, status_code=500)


if __name__ == "__main__":
    uvicorn.run("test:app", host="0.0.0.0", port=1111, reload=True)


It works. I open the browser and call the endpoint / and try to access http://127.0.0.1:1111/ , it will return the json {"detail":"test_error"} with HTTP code "500 Internal Server Error" .

500 on browser 500 in IDE

However, when I only changed StarletteHTTPException to Exception in the @app.exception_handler,

import uvicorn

from fastapi import FastAPI
from starlette.requests import Request
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import JSONResponse

app = FastAPI()


@app.exception_handler(Exception)
async def exception_callback(request: Request, exc: Exception):
    print("test")
    return JSONResponse({"detail": "test_error"}, status_code=500)


if __name__ == "__main__":
    uvicorn.run("test:app", host="0.0.0.0", port=1111, reload=True)

The method exception_callback could not capture the StarletteHTTPException when I accessed the http://127.0.0.1:1111/ . It reported 404 error.

404 on browser 404 in IDE

The excepted behaviour should be: StarletteHTTPException error could be captured by the method exception_handler decorated by Exception because StarletteHTTPException is the child class of Exception.

However, it is a known issue reported in Fastapi and Starlette

So we are not able to acheieve the goal currently.

Punch answered 1/3, 2021 at 3:8 Comment(1)
This was fixed by StarletteSalable
D
4

I found a way to catch exceptions without the "Exception in ASGI application_" by using a middleware. Not sure if this has some other side effect but for me that works fine! @iedmrc

@app.middleware("http")
async def exception_handling(request: Request, call_next):
    try:
        return await call_next(request)
    except Exception as exc:
        log.error("Do some logging here")
        return JSONResponse(status_code=500, content="some content")
Donative answered 22/7, 2021 at 8:55 Comment(0)
O
3

First I invite to get familiar with exception base classes in python. You can read them in the document Built-in Exceptions

Secondly, read through fastApi default exception overriding behaviour Override the default exception handlers

What you must understand is that @app.exception_handler accepts any Exception or child classes derived from Exception. For example RequestValidationError is a subclass of python built in ValueError which itself a subclass of Exception.

So you must design your own exceptions or throw available exceptions with this background. I guess what went wrong is with your logger logger.error(exc.detail) by either not having a detail field or not having a proper logger configuration.

Sample code :

@app.get("/")
def read_root(response: Response):
    raise ArithmeticError("Divide by zero")


@app.exception_handler(Exception)
async def validation_exception_handler(request, exc):
    print(str(exc))
    return PlainTextResponse("Something went wrong", status_code=400)

Output :

A stdout entry and a response with Something went wrong

Ovariectomy answered 5/5, 2020 at 7:59 Comment(3)
This code doesn't work for me. Unless I change the exception handler to @app.exception_handler(ArithmeticError), which is what OP is describing (parent class Exception not catching derived classes). I am not sure if this is a working solution.Later
For me it works (I get to the handler for ValueError) but remember that this doesn't catch an exception so exception will propagate further.Salable
First I invite you to get familiar with the question. You can realize that the OP is already doing same thing that you suggested. Secondly read through what exactly OP is asking. They are proficient and don't need consultation on Python basics. What you must understand is that "debug" mode of FastAPI is catching exceptions prior to such a handler.Trilobate
C
0

Subclass starlette.middleware.exceptions.ExceptionMiddleware, then override _lookup_exception_handler().

This answer was inspired by reading this method: starlette.applications.Starlette.build_middleware_stack()

Example:

class GenericExceptionMiddleware(ExceptionMiddleware):

    # Intentional: Defer __init__(...) to super class ExceptionMiddleware

    # @Override(ExceptionMiddleware)
    def _lookup_exception_handler(
            self, exc: Exception
    ) -> Optional[Callable]:
        if isinstance(exc, HTTPException):
            return self.__http_exception_handler
        else:
            return self.__exception_handler

    @classmethod
    async def __http_exception_handler(cls, request: fastapi.Request,  # @Debug
                                       ex: HTTPException):

        log.error("Unexpected error", cause=ex)
        resp = PlainTextResponse(content=f"Unexpected error: {ex.detail}"
                                         f"\n"
                                         f"\nException stack trace"
                                         f"\n====================="
                                         f"\n{ex}", # Improve to add full stack trace
                                 status_code=ex.status_code)
        return resp

    @classmethod
    async def __exception_handler(cls, request: fastapi.Request,  # @Debug
                                  ex: Exception):

        log.error("Unexpected error", cause=ex)
        resp = PlainTextResponse(content=f"Unexpected error: {ex}"
                                         f"\n"
                                         f"\nException stack trace"
                                         f"\n====================="
                                         f"\n{ex}", # Improve to add full stack trace
                                 status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR)
        return resp

Sample usage:

fast_api = FastAPI()
fast_api.add_middleware(GenericExceptionMiddleware, debug=fast_api.debug)
Condescending answered 23/3, 2023 at 12:2 Comment(0)
C
-1

I found and implemented this global handler for fast api for giving custom message for 429 status code:

@app.exception_handler(429)
async def ratelimit_handler(request: Request, exc: Exception):
    return JSONResponse({'message': "You have exceeded your request quota. Kindly try after some time.", 'status': 'failed'})
Crave answered 30/8, 2022 at 12:41 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Password

© 2022 - 2024 — McMap. All rights reserved.