Automatic error pages in Flask

Posted on Tue 08 September 2015 in Code • Tagged with Flask, Python, HTTP, errorsLeave 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.


  1. 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. 

Continue reading

Custom errors in Jinja templates

Posted on Sun 23 August 2015 in Code • Tagged with Jinja, templates, Python, errorsLeave a comment

Jinja is a pretty great templating engine for Python. Unlike the likes of Mustache or Handlebars, it doesn’t try to constrain overmuch the kind of logic that the programmer can put in their templates. Most Python expressions, for example, are supported, as are variables and functions (or macros in Jinja’s parlance).

One conspicously absent feature is throwing exceptions directly from template code. (It’s obviously possible from custom filters or tags). If we had it, we could add some more robust error checking to the template input values or macro paramaters.

Example

Let’s say you have extracted a {% macro %} for rendering a button, like the one from Twitter Bootstrap:

{% macro button(text=none, style='default', type='button') %}
  <button class="btn btn-{{ style }}" type="{{ type }}">
    {% if text is not none %}
      {{ text }}
    {% else %}
      {{ caller() }}
    {% endif %}
  </button>
{% endmacro %}

The benefit of this conditional definition is the relative ease of creating buttons with either just a text label:

{{ button("Load More...") }}

or some more complicated markup:

{% call button(style='success', type='submit') %}
  <i class="fa fa-check"></i>Finish
{% endcall %}

Supplying both, however, is an ambiguity that the macro currently “resolves” by preferring text over a {% call %} block. It should rather be an error instead:

{% if caller and caller is defined and text is not none %}
  {% error "Cannot supply text= parameter to button() when invoking it through {% call %}" %}
{% endif %}

But {% error %} isn’t a tag that Jinja supports by default. To make it available, we need to implement the logic ourselves.

Adding new Jinja tags

Don’t sweat, though! It won’t be necessary to fork Jinja itself and modify its core code. Custom tags may just as well be added with extensions, which is a dedicated Jinja mechanism.

An extension is just a Python class that declares tags it intends to support and implements a parse method:

from jinja2.ext import Extension

class ErrorExtension(Extension):
    tags = frozenset(['error'])

    def parse(self, parser):
        # ...

What can be a little tricky is that parse method itself shouldn’t take any direct action. Instead, it shall return a piece of ASTabstract syntax tree — that Jinja will include in the compiled template. All regular Jinja features are thus available, but the power of extensions doesn’t end here.

Handling the {% error %} tag

The argument to parse is a parser object, which we can use to extract tokens from the template source code. The error tag identifier is one such token, and the string following it is another. As it turns out, we are only tenuously interested in the former, but we definitely want to pick the latter, because it’s the error message we’ll raise an exception with:

tag = parser.stream.next()
message = parser.parse_expression()

As it was mentioned previously, the parse method itself won’t be raising this exception immediately. If we did that, no template with the {% error %} tag would ever parse successfully! What we need instead is an AST node that throws the exception when it’s evaluated. This way, it will be deferred until the template is being rendered and its execution point reaches the {% error "..." %} stanza.

Jinja already has a correct node type handy: it’s named CallBlock, and it represents a statement that {% call %}s given method of the Extension class:

from jinja2.nodes import CallBlock, Const

# (in ErrorExtension.parse)
node = CallBlock(
    self.call_method('_exec_error', [message, Const(tag.lineno)]),
    [], [], [])
node.set_lineno(tag.lineno)
return node

This method, _exec_error, should do the crux of what this extension is about: raise an exception. Let’s define a distinct error class for it, to tell user errors apart from those thrown by Jinja itself:

from jinja2 import TemplateAssertionError

class ErrorExtension(Extension):
    # ...
    def _exec_error(self, message, lineno, caller):
        raise TemplateUserError(message, lineno)

class TemplateUserError(TemplateAssertionError):
    pass

Using the extension

The last step is to tell Jinja to use our extension when compiling and rendering templates. For that, we simply add it to the Environment:

jinja_env.add_extension(ErrorExtension)

If you are using the Flask framework, jinja_env is an attribute on the Flask application object.

To see the complete code sample for ErrorExtension, have a look at this gist.

Continue reading