40

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.

Chris
  • 18,724
  • 6
  • 46
  • 80
Ajeet Singh
  • 717
  • 2
  • 6
  • 13
  • 1
    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`. – Maarten Derickx Jun 11 '21 at 19:43
  • Related answers can be found [here](https://stackoverflow.com/a/71682274/17865804) and [here](https://stackoverflow.com/a/71800464/17865804), as well as [here](https://stackoverflow.com/a/70954531/17865804) and [here](https://stackoverflow.com/a/72833284/17865804) – Chris Apr 21 '23 at 06:48

8 Answers8

38

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.

Neykuratick
  • 127
  • 4
  • 14
AndreFeijo
  • 10,044
  • 7
  • 34
  • 64
  • 4
    This one awaits for all task to finish. Thus, it prevents to schedule a task to run in background. – iedmrc Sep 24 '20 at 08:18
  • 2
    the 'middleware' example doesn't work for me but the usage of 'route' in the official documentation works like a charm https://fastapi.tiangolo.com/advanced/custom-request-and-route/#accessing-the-request-body-in-an-exception-handler – James H Apr 29 '21 at 21:17
  • We are using Peewee as ORM tool. Peewee does not support async. How can we still use this? – Florian Aug 25 '23 at 08:50
12

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
from fastapi.responses import JSONResponse

app = FastAPI()

@app.exception_handler(Exception)
async def validation_exception_handler(request, err):
    base_error_message = f"Failed to execute: {request.method}: {request.url}"
    # Change here to LOGGER
    return JSONResponse(status_code=400, content={"message": f"{base_error_message}. Detail: {err}"})
Akihero3
  • 365
  • 5
  • 12
8

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/

madox2
  • 49,493
  • 17
  • 99
  • 99
  • Note that `@app.exception_handler`s 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`. – shadowtalker Sep 14 '22 at 16:22
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.

zhfkt
  • 2,415
  • 3
  • 21
  • 24
5

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

Kavindu Dodanduwa
  • 12,193
  • 3
  • 33
  • 46
  • 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. – Andrey Apr 13 '21 at 23:53
  • 1
    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. – Konstantin Smolyanin Aug 17 '23 at 21:35
3

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")
Chris
  • 476
  • 4
  • 10
0

I was searching for global handler for fast api for giving custome message for 429 status code i found and implemented, working fine for me @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'})

  • 3
    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](/help/how-to-answer). – jasie Sep 06 '22 at 12:32
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)
kevinarpe
  • 20,319
  • 26
  • 127
  • 154