Customize error pages on a multisite Wagtail

Ash_Crow Python , Webdesign , Software , Website's life Français

Among the things to do to complete the creation of a website, customizing error pages, without being very high on the list, is not to be neglected. The 404 page (Not found), in particular, is a place where private jokes and Easter eggs are frequently hidden, and lists of the best 404 pages have become commonplace on web design blogs/sites. The same is true, to a lesser extent, of 403 (Forbidden) and 500 (Internal server error) error pages.

I've recently customized error pages on this site, and it seemed a good occasion to review the creation of custom error pages in this context.

HTTP status codes

First, a quick reminder about HTTP status codes: when you try to access a web page, in addition to the content of the page itself, you receive a 3-digit status code in return, with the first digit indicating the type (1xx = information, 2xx = success, 3xx = redirection, 4xx = client error, 5xx = server error). There are many of these, some of them more or less outlandish, but only a handful of the 4xx and 5xx messages are generally given specific treatment.

Error pages in Django / Wagtail

Wagtail inherits Django's error handling, which in addition to a fairly detailed debug system in development mode, provides views that make it easy to customize 4 error messages:

  • 404 (Not found)
  • 500 (Internal server error)
  • 403 (Forbidden)
  • 400 (Bad request)

I spent some time hesitating over the look I wanted to give these pages, and finally decided to give them a uniform, slightly glitchy/cyberpunk look based on a design found on Codepen, with a specific background image for each error. This allowed me to use a single common SCSS file for all 4 pages.

Error 500

The easiest page to deal with was 500, because I wanted a page with only HTML+CSS, without using Django's template functions. All is needed is to create a file titled 500.html at the root of the main application's templates folder, and Django is able to find it all by itself.

Error 500 page

Error pages 403 and 404

For the 403 and 404 pages, I wanted a page that was more integrated with the site interface, and in particular that would differ depending on the site used, since my instance of Wagtail manages sites with two different skins.

A thread on StackOverflow[1] points to a solution that is unfortunately not functional (or no longer) and has apparently been removed from the Wagtail doc. It did, however, point me in the right direction.

The first thing to do is to tell Django which views should handle 403 and 404 errors:

# config/urls.py

# [...]
handler403 = "portfolio.views.custom_403_view"
handler404 = "portfolio.views.custom_404_view"

(I decided to place these views in the portfolio app, since it's the one with the distinct theme from the others. In principle, this would have worked in any app).

The next step is to create said views. Since they essentially do the same thing, just with a different error code, we'll define the logic in a method that both views will call:

# portfolio/views.py


def custom_error_view(request, exception, error_code=404):
    """
    Returns a different error view depending on the site.
    """
    portfolio_site = Site.objects.filter(site_name="Portfolio pro").first()

    if portfolio_site:
        portfolio_hostname = portfolio_site.hostname
        # We can't use request.site, which is not served on an error view.
        if portfolio_hostname in request.environ.get("HTTP_HOST", ""):
            return render(
                request,
                f"portfolio/{error_code}.html",
                context={"exception": exception},
                status=error_code,
            )

    # Render the standard error page by default
    return render(
        request,
        f"{error_code}.html",
        context={"exception": exception},
        status=error_code,
    )


def custom_403_view(request, exception=None):
    """
    Returns a different 403 view depending on the site.
    """
    return custom_error_view(request, exception, error_code=403)


def custom_404_view(request, exception=None):
    """
    Returns a different 404 view depending on the site.
    """
    return custom_error_view(request, exception, error_code=404)

Compared with the proposed solution on StackOverflow, we can see a few changes:

  • I check that the portfolio site actually exists, which is useful for unit testing.
  • I do the comparison with request.environ.get("HTTP_HOST", "") and not with request.site.hostname, the latter variable being unavailable in an error page, which caused the site to return a 500 error (with no way of seeing the underlying problem, since none of this is accessible when DEBUG is activated), including on pages that should have worked (e.g. the welcome page when no language is selected).
Screenshot showing a "404" in large yellow characters, followed by a text indicating that the page could not be found. A photo of a spyglass, darkened and intertwined with dark lines, appears in the background. We see the main site skin, black with yellow accents.
Same as the previous image, but this time we see the portfolio skin, in black and white.

Comparison of two 404 pages (move slider to display either image)

Errors 400 and 502/503/504

For error 400, I've done the same thing as for error 500, but since most of the time, when it occurs, it doesn't even happen to Django, I've made sure that Nginx can also serve it. In the same spirit, I've made a page that covers errors 502 (Bad Gateway), 503 (Service Unavailable) and 504 (Gateway Time-out), likely to occur if Nginx is running but Django or Gunicorn aren't responding. These are the following lines in the Nginx configuration:

error_page 400 /templates/400.html;
error_page 502 503 504 /templates/50x.html;
location /templates {
        root <projectpath>/config;
}

location /errortesting {
        proxy_pass http://unix:/incorrect/path.sock;
} # This page should render the error page even if the site is properly loaded

The last block is used to test that the error page works correctly. For the 400, a simple '%' character at the end of the URL is enough to trigger it.

Login page

As an aside, I've also customized the Wagtail login page. This is done by creating a dashboard/templates/wagtailadmin/login.html page and redefining the relevant blocks:

{% extends "wagtailadmin/login.html" %}
{% load i18n wagtailadmin_tags %}

{% block branding_login %}
Attention
{% endblock %}

{% block login_form %}
    <p class="restricted-access">{% translate "Restricted access" %}</p>
    {{ block.super }}
{% endblock %}

{% block branding_logo %}
    <marquee aria-hidden="true">
        This place is a <em>message</em>... and part of a system of messages... <em>pay attention to it!</em>
        [...]
        The danger is unleashed only if you substantially disturb this place physically. <em>This place is best shunned and left uninhabited.</em>
    </marquee>
{% endblock %}

{% block css %}
    <link rel="stylesheet" href="{% versioned_static 'wagtailadmin/css/core.css' %}">
    <link rel="stylesheet" href="{% versioned_static 'css/style_login.css' %}">
    {% hook_output 'insert_global_admin_css' %}
    {% hook_output 'insert_editor_css' %}
{% endblock %}

(Yes, I've put the long-term nuclear waste warning messages in a <marquee> tag, and no, I'm not ashamed :D - but I've tried to reproduce the way <marquee> works with CSS not considered obsolete by the W3C, and I haven't succeeded...)

Fly, you fools

Header image:

A green tree python from the Ménagerie du Jardin des Plantes zoo in Paris (own work, CC-BY-SA 4.0)

0 comments posted.

Comments

Comments are closed.