Automatic error pages in Flask
Posted on Tue 08 September 2015 in Code • Tagged with Flask, Python, HTTP, errors • Leave a comment
In Flask, a popular web framework for Python, it’s pretty easy to implement custom handlers
for specific HTTP errors. All you have to do is to write a function and annotate it with the @errorhandler
decorator:
@app.errorhandler(404)
def not_found(error):
return render_template('errors/404.html')
Albeit simple, the above example is actually very realistic. There’s rarely anything else to do in response to serious request error than to send back some HTML with an appropriate message. If you have more handlers like that, though, you’ll notice they get pretty repetitive: for each erroneous HTTP status code (404, 403, 400, etc.), pick a corresponding template and simply render it.
Which is, of course, why we’d want to deal with all in a little smarter way and with less boilerplate.
Just add a template
Ideally, we would like to avoid writing any Python code at all for each individual error handler. Since all we’re doing revolves around predefined templates, let’s just define the handlers automatically based on the HTML files themselves.
Assuming we store them in the same template directory — say, errors/
— and name their files after
numeric status codes (e.g. 404.html), getting all those codes is quite simple1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | from pathlib import Path, PurePath
from mywebapp import app
#: Path to the directory where HTML templates for error pages are stored.
ERRORS_DIR = PurePath('errors')
def get_supported_error_codes():
"""Returns an iterable of HTTP status codes for which we have
a custom error page templates defined.
"""
error_templates_dir = Path(app.root_path, app.template_folder, ERRORS_DIR)
potential_error_templates = (
entry for entry in error_templates_dir.glob('*.html')
if entry.is_file())
for template in potential_error_templates:
try:
code = int(template.stem) # e.g. 404.html
except ValueError:
pass # could be some base.html template, or similar
else:
if code < 400:
app.logger.warning(
"Found error template for non-error HTTP status %s", code)
continue
yield code
|
Once we have them, we can try wiring up the handlers programmatically and making them do the right thing.
One function to handle them all
Although I’ve used a plural here, in reality we only need a single handler, as it’ll be perfectly capable of
dealing with any HTTP error we bind it to. To help distinguishing between the different status codes,
Flask by default invokes our error handlers with an
HTTPException
argument
that has the code
as an attribute:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | from flask import render_template
from jinja2 import TemplateNotFound
def error_handler(error):
"""Universal handler for all HTTP errors.
:param error: :class:`~werkzeug.exceptions.HTTPException`
representing the HTTP error.
:return: HTTP response to be returned
"""
code = getattr(error, 'code', None)
if not code:
app.logger.warning(
"Got spurious argument to HTTP error handler: %r", error)
return FATAL_ERROR_RESPONSE
app.logger.debug("HTTP error %s, rendering error page", code)
template = ERRORS_DIR / ('%s.html' % code)
try:
return render_template(str(template)), code
except TemplateNotFound:
# shouldn't happen if the handler has been wired up properly
app.logger.fatal("Missing template for HTTP error page: %s", template)
return FATAL_ERROR_RESPONSE
#: Response emitted when an error occurs in the error handler itself.
FATAL_ERROR_RESPONSE = ("Internal Server Error", 500)
|
Catching TemplateNotFound
exception is admittedly a little paranoid here, as it can occur pretty much exclusively
due to a programming error elsewhere. Feel free to treat it as a failed assertion about application’s internal state
and e.g. convert to AssertionError
if desirable.
The setup
The final step is to actually set up the handler(s):
for code in get_supported_error_codes():
app.errorhandler(code)(error_handler)
It may look a bit wonky, but it’s just a simple desugaring of the standard Python decorator syntax from the first example.
There exists a more direct approach of putting the handler inside app.error_handler_spec
dictionary,
but it is discouraged by Flask documentation.
Where to put the above code, though? I posit it’s perfectly fine to place in the module’s global scope, because the error handlers (and other request handlers) are traditionally defined at import time anyway.
Also notice that the default error handling we’ve defined here doesn’t preclude more specialized variants
for select HTTP codes. All you have to do is ensure that your custom @app.errorhandler
definition occurs
after the above loop.
-
Note that I’m using the pathlib module here — and you should, too, since it’s all around awesome. However, it is only a part of the standard library since Python 3.4, so you will most likely need to get the backport first. ↩