Error Handling¶
The error handling in APIFlask is based on the following basic concepts:
- All the automatic errors (404, 405, 500) will be in JSON format by default.
- Errors are built on top of the
HTTPErrorbase exception class. - Use
APIFlask.abort()function or raiseHTTPErrorclasses to generate an error response. - Use
app.error_processor(appis an instance ofapiflask.APIFlask) to register a custom error response processor. - Use
auth.error_processor(authis an instance ofapiflask.HTTPBasicAuthorapiflask.HTTPTokenAuth) to register a custom auth error response processor. - Subclass
HTTPErrorto create custom error classes for your errors.
Tip
The error handler registered with app.errorhandler for specific HTTP errors will be
used over the custom error response processor registered with app.error_processor.
Automatic JSON error response¶
In Flask, for 400/404/405/500 errors, a default error response will be generated. The default error response will be in HTML format with a default error message and error description. However, in APIFlask, these errors will be returned in JSON format with the following preset fields:
message: The HTTP reason phrase or a custom error description.detail: An empty dict (404/405/500) or the error details of the request validation (400).
You can control this behavior with the json_errors parameter when creating the APIFlask
instance, and it defaults to True:
from apiflask import APIFlask
# this will disable the automatic JSON error response
app = APIFlask(__name__, json_errors=False)
You can use the app.error_processor decorator to register a custom error processor
to customize the error response body. See more details
here.
Make an error response with abort and HTTPError¶
There are two ways to abort the request handling process and return an error response in the view function:
- Call the
abortfunction
Just like what you do in a normal Flask view function, but this abort function is
provided by APIFlask:
from apiflask import abort
@app.route('/')
def hello():
abort(400, message='Something is wrong...')
return 'Hello, world!' # this line will never be reached
It will raise an HTTPError behind the scene, so it will take the same arguments (see below).
- Raise the
HTTPErrorclass
Raise HTTPError will do the same thing:
from apiflask import HTTPError
@app.route('/')
def hello():
raise HTTPError(400, message='Something is wrong...')
return 'Hello, world!' # this line will never be reached
The call will generate an error response like this:
{
"message": "Something is wrong...",
"detail": {}
}
See HTTPError's API docs for
all the parameters you can pass to abort and HTTPError.
The extra_data is useful when you want to add more fields to the response body, for example:
abort(
400,
message='Something is wrong...',
extra_data={
'docs': 'http://example.com',
'error_code': 1234
}
)
{
"message": "Something is wrong...",
"detail": {},
"docs": "http://example.com",
"error_code": 1234
}
In most cases, you should create custom error classes with preset values instead of passing them to
abort or HTTPError directly. See more details in the section below.
Custom error classes¶
Version >= 0.11.0
This feature was added in the version 0.11.0.
To reuse errors, you can create custom error classes with preset error information. The
custom error classes should be inherited from HTTPError, and you can use the following attributes
in the error class:
- status_code
- message
- detail
- extra_data
- headers
Here is a simple example:
from apiflask import HTTPError
class PetNotFound(HTTPError):
status_code = 404
message = 'This pet is missing.'
extra_data = {
'error_code': '2323',
'error_docs': 'https://example.com/docs/missing'
}
Then you can raise this exception class in your view function:
@app.get('/pets/<pet_id>')
def get_pet(pet_id):
pets = [1, 2, 3]
if pet_id not in pets:
raise PetNotFound
return {'message': 'Pet'}
Use exception classes from Werkzeug
If you didn't set the json_errors to False when creating app instance,
APIFlask will catch all the Werkzeug exceptions, including the one you raised
directly:
from werkzeug.exceptions import NotFound
@app.get('/')
def say_hello():
if user is None:
raise NotFound
return {'message': 'Hello!'}
However, the description and body of the exception will be discarded.
Custom error status code and description¶
The following configuration variables can be used to customize the validation and authentication errors:
VALIDATION_ERROR_DESCRIPTIONAUTH_ERROR_DESCRIPTIONVALIDATION_ERROR_STATUS_CODEAUTH_ERROR_STATUS_CODE
See the Response customization section in the configuration docs for the details.
Custom error response processor¶
You can use the app.error_processor decorator to register a custom error response
processor function. It's a global error processor for all HTTP errors.
The decorated callback function will be called in the following situations:
- Any HTTP exception is raised by Flask when
APIFlask(json_errors=True)(default). - A validation error happened when parsing a request.
- An exception triggered with
HTTPError - An exception triggered with
abort.
You can still register a specific error handler for a specific error code or
exception with the app.errorhandler(code_or_exception) decorator. In that case,
the return value of the error handler will be used as the response when the
corresponding error or exception happens.
The callback function must accept an error object as an argument and return a valid response:
from apiflask import APIFlask
app = APIFlask(__name__)
@app.error_processor
def my_error_processor(error):
return {
'status_code': error.status_code,
'message': error.message,
'detail': error.detail
}, error.status_code, error.headers
The error object is an instance of HTTPError,
so you can get error information via its attributes:
- status_code: If the error is triggered by a validation error, the value will be
422 (default) or the value you passed in config
VALIDATION_ERROR_STATUS_CODE. If the error is triggered byHTTPErrororabort, it will be the status code you passed. Otherwise, it will be the status code set by Werkzueg when processing the request. - message: The error description for this error, either you passed or grabbed from Werkzeug.
-
detail: The detail of the error. When the validation error happens, it will be filled automatically in the following structure:
"<location>": { "<field_name>": ["<error_message>", ...], "<field_name>": ["<error_message>", ...], ... }, "<location>": { ... }, ...The value of
locationcan bejson(i.e., request body),query(i.e., query string) or other values depending on the place where the validation error happened (it matches the value you passed inapp.input). -
headers: The value will be
{}unless you pass it inHTTPErrororabort. - extra_data: Additional error information passed with
HTTPErrororabort.
If you want, you can rewrite the whole response body to anything you like:
@app.error_processor
def my_error_processor(error):
body = {
'error_message': error.message,
'error_detail': error.detail,
'status_code': error.status_code
}
return body, error.status_code, error.headers
Tip
I would recommend keeping the error.detail data in the response since it contains
the detailed information about the validation error when it happened.
After you change the error response, you have to update the corresponding OpenAPI schema for error responses so the API docs will match your custom error response schema.
Update the OpenAPI schema of error responses¶
There are two error schemas in APIFlask: one for generic errors (including auth errors),
and one for validation errors. They can be configured with HTTP_ERROR_SCHEMA and
VALIDATION_ERROR_SCHEMA, respectively.
Why do we need two schemas for error responses?
The reason behind a separate schema for the validation error response is that the detail
field of the validation errors will always have values. While for generic HTTP errors,
the detail field will be empty unless you passed something with HTTPError and
abort.
When you change the error response body with error_processor, you will also need
to update the error response schema, so it will update the OpenAPI spec of the error
response. The schema can be a dict of OpenAPI schema or a marshmallow schema class.
Here is an example that adds a status_code field to the default error response
and renames the existing fields (with OpenAPI schema dict):
# use the built-in `validation_error_detail_schema` for the `detail` field
from apiflask import APIFlask
from apiflask.schemas import validation_error_detail_schema
# schema for generic error response, including auth errors
http_error_schema = {
"properties": {
"error_detail": {
"type": "object"
},
"error_message": {
"type": "string"
},
"status_code": {
"type": "integer"
}
},
"type": "object"
}
# schema for validation error response
validation_error_schema = {
"properties": {
"error_detail": validation_error_detail_schema,
"error_message": {
"type": "string"
},
"status_code": {
"type": "integer"
}
},
"type": "object"
}
app = APIFlask(__name__)
app.config['VALIDATION_ERROR_SCHEMA'] = validation_error_schema
app.config['HTTP_ERROR_SCHEMA'] = http_error_schema
Handling authentication errors¶
When you set the json_errors to True when creating the APIFlask instance (defaults to True),
APIFlask will return JSON errors for auth errors and use the built-in errors callback or the
error processor you created with app.error_processor.
In the following situations, you need to register a separate error processor for auth errors:
- If you want to make some additional process for 401/403 error, instead of using
app.errorhandler(401)orapp.errorhandler(403)to register a specific error handler, you have to useauth.error_processorto register an auth error processor. - If you have set
json_errorstoFalse, but also want to customize the error response, you also need to register a custom auth error processor since the global error processor will not be used.
You can use the auth.error_processor decorator to register an auth error processor. It
works just like app.error_processor:
from apiflask import HTTPTokenAuth
auth = HTTPTokenAuth()
@auth.error_processor
def my_auth_error_processor(error):
body = {
'error_message': error.message,
'error_detail': error.detail,
'status_code': error.status_code
}
return body, error.status_code, error.headers
If you registered an auth error processor when json_error is True, it will overwrite the
global error processor.
Why do we need a separate error processor for auth errors?
APIFlask's authentication feature is backed with Flask-HTTPAuth. Since Flask-HTTPAuth uses a separate error handler for its errors, APIFlask has to add a separate error processor to handle it. We may figure out a simple way for this in the future.