Fastapi: [QUESTION] Handling Exception Response format

Created on 20 Feb 2020  路  20Comments  路  Source: tiangolo/fastapi

Hello.
I want to change the validation error response and make it inside app object.

I've found this example:

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
    )

But can't understand how to add this function to app object without decorator:

app = FastAPI(
    title='Bla API',
    description='Bla description',
    APIRoute('/api', api.toggle, methods=['POST'],
             description='Switch interface state',
             response_description='Interface successfully switched',
             response_class=JSONResponse,
             response_model=api.Success,
             responses={**api.post_responses},
             ),
...

question

All 20 comments

Why is the use of decorators problematic? Defininig pretty much anything inside the FastAPI constructor like that is certainly an uncommon way to do things and much of the discussion in #687 was about how that approach would be likely to be less ergonomic for routes when taking FastAPI's goals into account (like how Path parameters would end up split between the route declaration and the function signature).

It's not currently possible to do that with exception_hadler with FastAPI right now (upstream Starlette versions support that, but it causes compatibility issues with FastAPI, see #683), but why do you want to do that with the exception handler?

I have an app that has both GUI (rendering HTML page) and API.
It is pretty convenient to keep code of web view in one module and API code in another one, then import these modules and build an app object.
This approach just keeps everything in its own place.

This is how I like it:


import api
from views import home

statics = StaticFiles(directory="statics", packages=["bootstrap4"])


app = FastAPI(
    title='Yandex.Cloud Netinfra API',
    description='A bundled API for Yandex.Cloud Netinfra team tools',
    routes=[
        Route('/', home, methods=['GET', 'POST']),
        APIRoute('/api', api.check, methods=['GET'], tags=['VlanToggler'],
                 # name='String replaces function name on Swagger API page',
                 # summary='String replaces function name AND name on Swagger API page',
                 description='Get current interface state',
                 response_description='Successfully got interface state',
                 response_class=JSONResponse,
                 response_model=api.Success,
                 responses={**api.get_responses}
                 ),
        APIRoute('/api', api.toggle, methods=['POST'], tags=['VlanToggler'],
                 description='Switch interface state',
                 response_description='Interface successfully switched',
                 response_class=JSONResponse,
                 response_model=api.Success,
                 responses={**api.post_responses},
                 ),
        Mount('/statics', statics, name='static'),
    ],
)

So you're defining each route in separate modules, importing each of them in your app module and then declaring your routes inside your constructor? Usually you would want to either create an APIRouter in these modules and do something like this:

from api import api_router
app.include_router(api_router, prefix='/api')

...or build secondary app objects for your API and your HTML views and mount those onto your primary app:

from api import api_app
app.mount('/api', api_app)

That code style looks a lot like the style Starlette 0.13 is moving to, you might want to read #687 to see why it tends to be problematic with FastAPI (though it still works fine for mounting routes and routers, nothing wrong with it).

Regarding exception handlers, though, you can't do that, at least not for the time being, as FastAPI still uses Starlette 0.12.9, specifically because putting exception handlers in the constructor proved to be a breaking change.

I didn't know I can mount one app to another. I can use this to keep API separated from VIEW module. Thanks for this tip.

Is it fine to mount FastAPI app object to Starlette app object?

Yes, though it's worth keeping in mind that Starlette and FastAPI don't propagate lifetime events to subapplications (#811), so if you're doing anything with @app.on_event("startup") or @app.on_event("shutdown") in the applications you're mounting, those will not run.

That unlikely usecase aside, everything should be fine.

If I understood the idea correctly this is how I should have done it:

GUI part:

<my imports> 

class FormSchema(typesystem.Schema):
    <my schema>

def render(request: Request, form=forms.Form(FormSchema), vl_status: Optional[str] = None) -> _TemplateResponse:
    context = {'request': request, 'form': form, 'vl_status': vl_status}
    return templates.TemplateResponse('index.html', context)


async def home(request: Request) -> _TemplateResponse:
    if request.method == "POST":
        <my gui app login>

    return render(request)


vlantoggler_web_app = Starlette(
    routes=[
        Route('/vlantoggler', home, methods=['GET', 'POST']),
        Mount('/statics', statics, name='static'),
    ],
)

Api part:

<my imports> 


class Status(str, Enum):
    ok = 'ok'
    error = 'error'


class State(str, Enum):
    prod = 'prod'
    setup = 'setup'
    unknown = 'unknown'
    l3 = 'l3'


<my other response classes>


get_responses = {
    400: {
        'description': 'ToR received invalid RPC',
        'model': Error
    },
    ...
}
post_responses = {
    403: {
        'description': 'Not allowed to switch interface',
        'model': Error
    },
    **get_responses
}


class GetDeviceData(BaseModel):
    tor: str
    interface: str


class PostDeviceData(BaseModel):
    tor: str
    interface: str
    state: DesiredState


async def check(data: GetDeviceData):
    <some logic>
        return JSONResponse(status_code=result['code'], content=result)


async def toggle(data: PostDeviceData):
    <some logic>
        return JSONResponse(status_code=result['code'], content=result)


vlantoggler_api_app = FastAPI(
    title='',
    description='',
    openapi_prefix='/myapp/api',
    routes=[
        APIRoute('/', check, methods=['GET'], tags=['VlanToggler'],
                 name='Get current interface state',
                 # summary='String replaces function name AND name on Swagger API page',
                 description='ToR and Interface names are validated and current interface state is returned',
                 response_description='Successfully got interface state',
                 response_class=JSONResponse,
                 response_model=Success,
                 responses={**get_responses}
                 ),
        APIRoute('/', toggle, methods=['POST'], tags=['VlanToggler'],
                 name='Switch interface state',
                 description='ToR and Interface names are validated and ToR interface is toggled to desired state',
                 response_description='Interface successfully switched',
                 response_class=JSONResponse,
                 response_model=Success,
                 responses={**post_responses},
                 ),
    ],
)


@vlantoggler_api_app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            'code': 422,
            'status': 'error',
            'message': '; '.join([f"{e['loc'][2]}: {e['msg']}" for e in exc.errors()])
        }
    )

And my start point

import uvicorn

from api import vlantoggler_api_app
from views import vlantoggler_web_app

app = vlantoggler_web_app
app.mount('/vlantoggler/api', vlantoggler_api_app)

if __name__ == "__main__":
    uvicorn.run(app, loop='uvloop', log_level='debug')

It works perfectly! =)

But I still have a question regarding validator: I rewrote exception_handler:

async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            'code': 422,
            'status': 'error',
            'message': '; '.join([f"{e['loc'][2]}: {e['msg']}" for e in exc.errors()])
        }
    )

and I have validation handling format I wanted:

{"code":422,"status":"error","message":"state: value is not a valid enumeration member; permitted: 'prod', 'setup'"}

But on Swagger page 422 status still has default schema:
image

How can I change it?

Thanks for all the help here @sm-Fifteen !

@horseinthesky you can override and customize the OpenAPI as described in: https://fastapi.tiangolo.com/advanced/extending-openapi/

There's is currently no other way to modify it in the generated OpenAPI schema.

@tiangolo I see how to change some simple things like title or icon but where is this Validation Error description located?

In the description I wrote that I did this:

@vlantoggler_api_app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            'code': 422,
            'status': 'error',
            'message': '; '.join([f"{e['loc'][2]}: {e['msg']}" for e in exc.errors()])
        }
    )

for the FASTApi object but it only changed the result:

{"code":422,"status":"error","message":"state: value is not a valid enumeration member; permitted: 'prod', 'setup'"}

but not the Swagger page description.

And this solution doesn't work with APIRouter object at all.

Is there a solution to change the APIRouter returning format?
And could you provide an example to change Swagger 422 Example Value?

may be you can try this:

"""For managing validation error for complete application."""
from typing import Union
from fastapi.exceptions import RequestValidationError
from fastapi.openapi.constants import REF_PREFIX
from fastapi.openapi.utils import validation_error_response_definition
from pydantic import ValidationError
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY


async def http422_error_handler(
    _: Request, exc: Union[RequestValidationError, ValidationError],
) -> JSONResponse:
    """To handle unprocessable request(HTTP_422) and log accordingly to file.

    Args:
        _ (Request): Request object containing metadata about Request
        exc (Union[RequestValidationError, ValidationError]): to handle RequestValidationError,
            ValidationError

    Returns:
        JSONResponse: JSON response with required status code.
    """
    return JSONResponse(
        {"error": exc.errors()}, status_code=HTTP_422_UNPROCESSABLE_ENTITY,
    )


validation_error_response_definition["properties"] = {
    "errors": {
        "title": "Errors",
        "type": "array",
        "items": {"$ref": "{0}ValidationError".format(REF_PREFIX)},
    },
}

And in main.py

app.add_exception_handler(RequestValidationError, http422_error_handler)

@sm-Fifteen @davemaharshi7
I'm sorry for bothering you. Could you provide an example of changing docs for ValidationError.

The code above does handling but not docs.

async def http422_error_handler(
    _: Request, exc: Union[RequestValidationError, ValidationError],
) -> JSONResponse:
    return JSONResponse(
        status_code=HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            'code': 422,
            'status': 'error',
            'message': '; '.join([f"{e['loc'][-1]}: {e['msg']}" for e in exc.errors()])
        }
    )


validation_error_response_definition["properties"] = {
    "errors": {
        "title": "Errors",
        "type": "array",
        "items": {"$ref": "{0}ValidationError".format(REF_PREFIX)},
    },
}

app = FastAPI(
    title='Yandex.Cloud Netinfra API',
    description='A bundled API for Yandex.Cloud Netinfra team tools',
)
...
<some routers>
...

app.add_exception_handler(RequestValidationError, http422_error_handler)

I get nice ValidationError handling:

HTTP/1.1 422 Unprocessable Entity
content-length: 79
content-type: application/json
date: Mon, 13 Jul 2020 10:01:28 GMT
server: uvicorn

{
    "code": 422,
    "message": "interface: Interface name is too long",
    "status": "error"
}

But docs are no good:
image

@horseinthesky

Hi, you could do it like this.


router = APIRouter()

class ValidationErrorModel(BaseModel):
    message: str
    status: Optional[int] = 422
    details: str

@router.get('/{user_id}', responses={422:  {'description': 'Validation Error', 'model': ValidationErrorModel}})
def get_by_id(user_id:int):
      # get_by_id
      # logic
      # here

or like this

app.include_router(router, prefix='/users',
                   responses={422: {'description': 'Validation Error', 'model': ValidationErrorModel}})

You can do that by updating this dictionary validation_error_response_definition

validation_error_response_definition["properties"] = {
    "errors": {
        "title": "Errors",
        "type": "array",
        "items": {"$ref": "{0}ValidationError".format(REF_PREFIX)},
    },
}

@MacMacky
I believe I've tried that before creating the issue and it didn't work back then.
Works great and as expected now. Thank you!

I have one more question regarding handling.
In my post above I did handling via adding handler function to an app object:

async def http422_error_handler(
    _: Request, exc: Union[RequestValidationError, ValidationError],
) -> JSONResponse:
    return JSONResponse(
        status_code=HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            'code': 422,
            'status': 'error',
            'message': '; '.join([f"{e['loc'][-1]}: {e['msg']}" for e in exc.errors()])
        }
    )

app = FastAPI(
    title='Yandex.Cloud Netinfra API',
    description='A bundled API for Yandex.Cloud Netinfra team tools',
)
...
<include some routers>
...

app.add_exception_handler(RequestValidationError, http422_error_handler)

Is it possible to do the same thing for not the whole App wide but APIRouter wide?
Something like this maybe?:
```python
async def some_router_function():
...

api_router = APIRouter(
routes=[
APIRoute('/test', some_router_function, methods=['GET'], tags=['VlanToggler'],
response_class=JSONResponse,
response_model=docs.Success,
responses={**docs.get_responses}
)
]
)
api_router.add_exception_handler(RequestValidationError, http422_error_handler)

app = FastAPI(
title='Yandex.Cloud Netinfra API',
description='A bundled API for Yandex.Cloud Netinfra team tools',
)
app.include_router(api_router,
prefix='/api')

@horseinthesky
In my second example above you can specify the custom responsesthat you want in a router

app.include_router(router, prefix='/users',
                   responses={422: {'description': 'Validation Error', 'model': ValidationErrorModel}})

@horseinthesky
In my second example above you can specify the custom responsesthat you want in a router

app.include_router(router, prefix='/users',
                   responses={422: {'description': 'Validation Error', 'model': ValidationErrorModel}})

I got this.

My question is: can I specify a custom 422 exception handler for an APIRouter not an FastAPI app?

app.add_exception_handler(RequestValidationError, http422_error_handler)

@horseinthesky
Awh, ok. I think you can only add custom exception handlers on the app not on a ApiRouter.

@MacMacky Ok. Thanks again for your help.

Was this page helpful?
0 / 5 - 0 ratings