How to catch or process RequestValidationError exceptions differently in a FastAPI middleware?

206 Views Asked by At

How to correctly combine RequestValidationError exception handling functions:

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
    response = prepare_response({}, g_ERROR__INCORRECT_PARAMS)

    return JSONResponse(content=response)

and the middleware code function:

@app.middleware("http")
async def response_middleware(request: Request, call_next):
    try:
        result = await call_next(request)

        res_body = b''
        async for chunk in result.body_iterator:
            res_body += chunk

        response = prepare_response(res_body.decode(), g_ERROR__ALL_OK)
    except Exception as e:
        response = prepare_response({}, g_ERROR__UNKNOWN_ERROR)

    return JSONResponse(content=response)

It is necessary that ONLY the RequestValidationError exception triggers the following function:

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):

and all other cases this function:

@app.middleware("http")
async def response_middleware(request: Request, call_next):

Now the response_middleware function fires all the time and processes the result of validation_exception_handler, which violates the basic intent of the function.

When using @app.middleware("http") any exceptions disappear and even without @app.exception_handler(RequestValidationError) the code consistently generates 200 OK and

    try:
        result = await call_next(request)

        res_body = b''
        async for chunk in result.body_iterator:
            res_body += chunk

        response = prepare_response(res_body.decode(), g_ERROR__ALL_OK)
    except RequestValidationError as e:
        response = prepare_response({}, g_ERROR__INCORRECT_PARAMS)
    except Exception as e:
        response = prepare_response({}, g_ERROR__UNKNOWN_ERROR)

also doesn't work at all after

result = await call_next(request)

no exceptions are thrown

How can this problem be solved?

On the one hand we need middleware code for different functions, on the other hand some exceptions (incorrect parameters, etc.) should be tracked before the intermediate code or inside it, but not deeper.

2

There are 2 best solutions below

0
On

You should re-raise RequestValidationError exception in your middleware.

Try changing your middleware code as shown below:

@app.middleware("http")
async def response_middleware(request: Request, call_next):
    try:
        result = await call_next(request)

        res_body = b''
        async for chunk in result.body_iterator:
            res_body += chunk

        response = prepare_response(res_body.decode(), g_ERROR__ALL_OK)
    except RequestValidationError as e:
        raise # Re-raise exception!
    except Exception as e:
        response = prepare_response({}, g_ERROR__UNKNOWN_ERROR)

    return JSONResponse(content=response)
0
On

One way to achieve that would be to raise any RequestValidationError exceptions in your custom validation_exception_handler, and then catch them, using a try-except block around await call_next(request) in the middleware—please take a look at this answer (Options 1 and 2), this answer (Option 4) and this answer (Option 1) for complete working examples.

However, the most straightforward way might be to simply check the status_code of the response, and if the HTTP response status code is in the range specified (e.g., between 400 and 599, indicating a client or server error), or if it is just 422 (as you mentioned that only RequestValidationError exceptions need to be processed differently from every other response/exception), then process the response accordingly. You might also benefit from looking at this answer, when dealing with a middleware in FastAPI.

Working Example

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

app = FastAPI()


@app.middleware("http")
async def custom_middleware(request: Request, call_next):
    response = await call_next(request)
    
    #if 399 < response.status_code < 600:
    if response.status_code == 422:
        # return Exception response as is
        return response
        
        # or return some custom response
        #return JSONResponse(status_code=422, content={"msg": "custom"})
        
    else: # do whatever
        return response
    

class Demo(BaseModel):
    content: str


@app.post('/')
async def main(demo: Demo):
    return demo