Handling HTTP errors with FastAPI#
FastAPI automatically returns some HTTP errors for the application, such as 422 errors when the input to a handler doesn’t match its requirements.
Other errors are normally returned by raising fastapi.HTTPException
with an appropriate status_code
parameter.
Errors raised by FastAPI will have a JSON body that follows a standard syntax. To provide standard error reporting for all application APIs, you may wish to return other errors using the same structure.
Raising structured errors#
fastapi.HTTPException
supports a detail
parameter that should include information about the cause of the error.
FastAPI accepts an arbitrary JSON-serializable data in that parameter, but for compatibility with the errors generated internally by FastAPI, the value should be an array of dict representations of safir.models.ErrorDetail
.
The loc
attribute of ErrorDetail
, if present, is an ordered list of location keys identifing the specific input data that caused the error.
The first element of loc
should be chosen from the values of safir.models.ErrorLocation
.
For example, for an error in the job_id
path variable, the value of loc
would be [ErrorLocation.path, "job_id"]
.
For an error in a nested account
element in the body, the value of loc
might be [ErrorLocation.body, "config", "account"]
.
loc
may be omitted for errors not caused by a specific element of input data.
The code to raise fastapi.HTTPException
should therefore look something like this:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=[
{
"loc": [ErrorLocation.path, "foo"],
"msg": msg,
"type": "invalid_foo",
},
],
)
Declaring the error model#
To declare that a particular error code returns safir.models.ErrorModel
, use the responses
attribute to either the route decorator or to include_router
if it applies to all routes provided by a router.
For example, for a single route:
@router.get(
"/route/{foo}",
...,
responses={404: {"description": "Not found", "model": ErrorModel}},
)
async def route(foo: str) -> None:
...
If all routes provided by a router have the same error handling behavior for a given response code, it saves some effort to instead do this when including the router, normally in main.py
:
app.include_router(
api.router,
prefix="/auth/api/v1",
responses={
403: {"description": "Permission denied", "model": ErrorModel},
},
)