How to force all exceptions to go through a FastAPI middleware?
Asked Answered
C

1

1

I am writing an app using the python fastapi library. Parts of my code raise different exceptions which I need to process. I want all that processing to be done in a single place (with different except blocks depending on what exception was raised). I tried to do this by adding the following (simplified) middleware to my FastAPI app:

from fastapi import APIRouter, FastAPI, Request, HTTPException, Response, status
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.responses import JSONResponse

class ExceptionHandlerMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        try:
            return await call_next(request)
        except HTTPException as e:
            return JSONResponse(status_code=e.status_code, content={'message': e.detail})
        except Exception as e:
            return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={'message': str(e)})

# app creation and config go here....

app.add_middleware(ExceptionHandlerMiddleware)

The problem is that this does not work for some specific exceptions which fastapi handles by default (for example if a HTTPException is raised, it will never reach the except block in my middleware because it will already be processed by default before reaching the middleware)

Fastapi documentation gives a way to override default behavior for specific exceptions, but this is tedious and not scalable. Is there a way to globally force all exception handling to go through my middleware? (bonus points if anyone knows a way to do this without using decorators, as one requirement I have is to not use them)

UPDATE: My question was initially closed as a duplicate of this question, however it is NOT the same thing. That question deals with catching unhandled exceptions, and in my question I am dealing with forcing the handling of exceptions that are handled by default (like HTTPException) to happen elsewhere in code that I control (as stated in my question, attempting to catch HTTPException in the middleware does not work! (which would be the takeaway from the linked question))

Clinkerbuilt answered 9/4 at 9:4 Comment(0)
S
1

Starlette's HTTPExceptions are not the same as errors, which can be handled using a try-except block; thereby, allowing one to handle any Exception (or child class derived from Exception) that is raised. You would thus need to have a custom exception handler for handling FastAPI/Starlette's HTTPExceptions.

Please note that the following example makes use of code and details explained in this answer, this answer, this answer, as well as this answer, this answer and this answer.

Also, I would highly suggest reading Starlette's documentation on exceptions and FastAPI's documentation on Starlette's HTTPException. As explained in FastAPI's documentation:

...

So, you can keep raising FastAPI's HTTPException as normally in your code.

But when you register an exception handler, you should register it for Starlette's HTTPException.

This way, if any part of Starlette's internal code, or a Starlette extension or plug-in, raises a Starlette HTTPException, your handler will be able to catch and handle it.

In this example, to be able to have both HTTPExceptions in the same code, Starlette's exceptions is renamed to StarletteHTTPException:

from starlette.exceptions import HTTPException as StarletteHTTPException 

Example

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


app = FastAPI() 


@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
    # Do some logging here
    print(exc.detail)
    return JSONResponse(content={"detail (specify as desired)": exc.detail}, status_code=exc.status_code)


@app.middleware("http")
async def exception_handling_middleware(request: Request, call_next):
    try:
        return await call_next(request)
    except Exception as e:
        # Do some logging here
        print(str(e))
        return JSONResponse(content="Something went wrong", status_code=500)
        
  
@app.get('/')
async def index():
    raise HTTPException(detail="Bad Request", status_code=400)
    

@app.get('/result')
async def get_result():
    return 1 + '1'  # this should raise an error


On a side note, since you mentioned that you wouldn't like using decorators, you could rewrite the above HTTPException handler, as follows:

...

async def http_exception_handler(request: Request, exc: StarletteHTTPException):
    # same logic as above
    pass

  
exception_handlers = {
    StarletteHTTPException: http_exception_handler
}

  
app = FastAPI(exception_handlers=exception_handlers)

...

As for using a middleware without a decorator as well, please have a look at Starlette's BaseHTTPMiddleware, or preferably, how to implement a pure ASGI Middleware.

Supersession answered 9/4 at 17:47 Comment(3)
Thank you for your answer, but that is still not what I am looking for. For example, suppose I set app.exception_handlers = {}. This should in theory force all errors (be it HTTPException or anything else) to go through my middleware, but this is not the case (maybe. a fastapi bug?). I do not want to specify per-exception-type error handling as your and the other answers state, but want to disable the fastapi default behavior completely, and force all exception handling to go to one place (my middleware)Clinkerbuilt
You could alternatively use a custom APIRoute class, as demonstrated in Option 2 of this answer. That would allow you to handle FastAPI/Starlette's HTTPExceptions inside the custom_route_handler as well, but every route in your API should be added to that router. I really don't see why using an exception_handler for StarletteHTTPException and a middleware for every other exception, as shown above, does not work for you.Supersession
It should also be noted that Starlette handles those exceptions through ExceptionMiddleware, but I wouldn't bother overriding/modifying that middleware; instead, I would use a custom exception_handler, as shown above.Supersession

© 2022 - 2024 — McMap. All rights reserved.