Flask API Exception Handling with Custom HTTP Response Codes

Ivan N.
Datasparq Technology
4 min readApr 3, 2020

How to return meaningful responses when something goes wrong.

TLDR:

  • Use custom Exceptions in your Flask app
  • Capture all of those and return them to the client in a uniform format in the response body
  • Have an SST (single source of truth) for your HTTP response codes

The problem

Has something like this ever happened to you?

An example Flask exception trace I found

Recently I was working on a RESTful API with Flask were multiple errors needed to be returned to the client and each error required a custom message and an HTTP code. They covered all kinds of scenarios:

  • Missing username/password
  • Incorrect credentials
  • Malformed POST data
  • Attempts to purchase products that were not available to the customer
  • Expired credentials
  • etc…

If the user was to attempt any of the above, they would receive a response that looked something like this:

{"error": "Incorrect Credentials"}

And you might be tempted to do something like this:

from flask import  Flask, jsonify, requestapp = Flask(__name__)@router.route("/user/login", methods=['POST'])
def user_login():
if "email" in request.data and "password" in request.data:
if check_user_exists(request.data["email"]):
pass
else:
return jsonify({"error": "Incorrect Email",}), 403

if check_password_is_correct(request.data["email"], request.data["password"]):
pass
else:
return jsonify({"error": "Incorrect Password",}), 403

else:
return jsonify({"error": "Missing Credentials",}), 403

return "Success", 200
app.run(debug=True)
☝️Not sure this is a great idea…

See how this solution has 3 different return statements for each specific error case. This might seem fine, until one fine day you realise that from then on you want to have a different response schema:

{"error": "Incorrect Credentials", "code": 403, "request_id": "1234567"}

Ok, maybe if you only have 3 instances to refactor it’s nothing to write home about. But imagine 300 across multiple files. Now that’s a task to keep an intern occupied for a week.

Why not use Exception

What I recommend you do is use the ability to raise custom Python exceptions:

#...class APIAuthError(Exception):
code = 403
description = "Authentication Error"
#...
@router.route("/user/login", methods=['POST'])
def user_login():
if "email" not in request.data:
raise APIAuthError('Missing email value')

if "password" not in request.data:
raise APIAuthError('Missing password value')

if check_user_exists(request.data["email"]) and check_password_is_correct(request.data["email"], request.data["password"]):
return "Success", 200
else:
raise APIAuthError('Incorrect Credentials')

There are several upsides to this:

(a) You keep the HTTP response (error) code in one single place (Single Source of Truth). This is great if you are compiling documentation automatically. In that scenario you can refer to the HTTP code as APIAuthError().code

(b) You can decouple the error parsing and schema from the API logic:

@app.errorhandler(APIAuthError)
def handle_exception(err):
"""Return JSON instead of HTML for MyCustomError errors."""
response = {
"error": err.description,
"request_id": get_request_id_from_somewhere()
}
if len(err.args) > 0:
response["message"] = err.args[0]
return jsonify(response), err.code

(c) You can do this for any exception class:

@app.errorhandler(SQLAlchemyError)
def handle_exception(err):
"""Handle DB connection errors """
if isinstance(err, sqlalchemy.exc.InternalError):
response["message"] = "Unable to connect to DB"
return jsonify(response), 555

I would also suggest having a separate Flask error handler that captures any other unforeseen obstacle your users may come across:

@app.errorhandler(500)
def handle_exception(err):
"""Return JSON instead of HTML for any other server error"""
app.logger.error(f"Unknown Exception: {str(err)}")
app.logger.debug(''.join(traceback.format_exception(etype=type(err), value=err, tb=err.__traceback__)))
response = {"error": "Sorry, that error is on us, please contact support if this wasn't an accident"}
return jsonify(response), 500

Summary and Code

I have compiled all of the code from this article in a single gist. It’s a barebones Flask app, but you can paste and run it:

Further reading

--

--

Ivan N.
Datasparq Technology

When the machines take over, I will be on the winning side 🤖