Handling Errors in Flask API Services

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. 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 developer. Here’re some examples.

On the contrary, application errors are exceptions raised intentionally by the developer. For example:

Flask provides default error handlers, but they return HTML, not JSON. Thus we may overwrite them.

# 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 error_handler gets e of ZeroDivisionError and tries to access the nonexistent fields description and code, thus it raises an exception in the 500-error handler. It causes Flask to send the default 500-error page.

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)