DONG Yuxuan @ Apr 13, 2019 Asia/Shanghai
Discuss the best practice of handling errors while use Flask to build an API service.
Nowadays most modern web applications are developed as SPAs, that means the server provides only the API which is often in the JSON format instead of rendering HTML. Flask as a great Python web framework can also be used to build the API service but its infrastructures for handling errors are designed for traditional web sites thus we need a usage pattern to make building API services using Flask more convenient.
Errors in Flask-based applications can be classified by two types. I called them system errors and application errors.
System errors are exceptions which are not intentionally raised by the application code. Here’re some examples.
An HTTPException with code 400 will be raised if the application tries to access a nonexistent field of
An HTTPException with code 404 will be raised if a nonexistent route is visited.
An HTTPException with code 500 will be raised if the application tries to access a field of the None object.
On the contrary, application errors are exceptions raised intentionally by the application code. For example:
The client tries to access a nonexistent resource of the RESTful service, the application will raise an exception by invoking
The client submitted a request to register an account but the application found the user name existed, an exception with code 403 or 409 with the message “Duplicated username” may be raised intentionally by the application code.
Flask provides default error handlers which may work well in a traditional web site but must be taken over in an API service because they’re in HTML, not JSON, thus we may overwrite them below.
# This snippet is to illustrate a bad example # DO NOT USE IT from flask import jsonify from werkzeug.exceptions import * def error_handler(e): return jsonify(e.description), e.code app.register_error_handler(BadRequest, error_handler) # 400-error app.register_error_handler(NotFound, error_handler) # 404-error app.register_error_handler(InternalServerError, error_handler) # 500-error # Maybe other HTTPException you want to catch here, like MethodNotAllowed
This seems good but there is a problem before Flask 1.1.0. If we raise a ZeroDivisionError like this:
# Before Flask 1.1.0 @app.route("/") def index(): return 0 // 0
You’ll find that the default 500-error page is sent to the client. The reason is that when no handler is registered for the exception, in this example, ZeroDivisionError, Flask will invoke the 500-error handler, but before 1.1.0 the parameter passed in the handler is of the original exception class instead of InternalServerError. In our example, the
ZeroDivisionError and tries to access the nonexistent fields
code, thus it raises an exception in the error handler and causes Flask to send the default 500-error page. For more details about how Flask handles errors you can read this article.
To fix this, we need an individual handler for 500-errors:
from flask import jsonify from werkzeug.exceptions import * def error_non500(e): return jsonify(e.description), e.code def error500(e): return jsonify("Internal Server Error"), 500 app.register_error_handler(BadRequest, error_non500) app.register_error_handler(NotFound, error_non500) app.register_error_handler(InternalServerError, error500) # Maybe other HTTPException you want to catch here, like MethodNotAllowed
System errors are now handled properly. To make these handlers have an effect on application errors, we need a convention. When we need an error response, we raise an HTTPException subclass. The status code is implied by the class and the message is in the
description parameter. Do not intentionally raise any exception which is not of the HTTPException subclass and do not directly return an error response like
return "Wrong", 403 in view functions. Let’s demonstrate it with the “Duplicated username” example:
# We can post a user resource to the RESTful service for registering. @app.route("/users", methods=["POST"]) def postuser() user = request.json; if is_existed(user["username"]): raise Forbidden(description="Duplicated username") # 403 Error # Or, if you prefer 409 # raise Conflict(description="Duplicated username") return register(user)