Personnalisation des pages d’erreur sur un Wagtail multisite
Parmi les choses à faire pour compléter la création d’un site web, la personnalisation des pages d’erreur, sans être très haut dans la liste, n’est pas à négliger. La page 404 (page non trouvée), notamment, est un endroit où sont fréquemment cachées des private jokes et des easter eggs, et les listes des meilleures pages 404 sont devenues des marronniers sur les blogs/sites de webdesign[1]. C'est également le cas, dans une moindre mesure, des pages 403 (accès refusé) et 500 (erreur interne du serveur)
Je me suis récemment lancé dans la personnalisation de plusieurs pages d’erreur de ce site, et cela me semblait un bon moment pour faire le point sur la création de pages d’erreur personnalisées dans ce contexte.
Codes HTTP
Pour commencer, petit rappel sur les codes HTTP : lorsque l’on tente d’accéder à une page web, on reçoit en retour, outre le contenu de la page proprement dite, un code d’état à 3 chiffres, le premier indiquant le type (1xx = information, 2xx = succès, 3xx = redirection, 4xx = erreur côté utilisateur, 5xx = erreur côté serveur) Il en existe de nombreux, dont certains plus ou moins farfelus, mais seuls une poignée des messages 4xx et 5xx ont généralement le droit à un traitement spécifique.
Pages d’erreur dans Django / Wagtail
Wagtail hérite de la gestion des erreurs de Django, qui outre un système assez poussé en mode développement, fournit des vues facilitant la personnalisation de 4 messages d’erreur :
- 404 (page non trouvée)
- 500 (erreur interne du serveur)
- 403 (accès refusé)
- 400 (requête incorrecte)
J’ai pas mal hésité sur le look à donner à ces pages, et j’ai fini par me décider pour les uniformiser avec un look un peu glitchy/cyberpunk basé sur un design trouvé sur Codepen, en adaptant l’image de fond à chaque erreur. Cela m’a permis d’utiliser un seul fichier SCSS commun pour les 4 pages.
Page d’erreur 500
La plus simple à gérer a été la page 500, parce que je voulais une page en HTML+CSS uniquement, sans faire appel aux fonctions de template de Django. Il suffit de créer un fichier titré 500.html
à la racine du dossier templates
de l’application principale, et Django est capable de le trouver tout seul.
Pages d’erreur 403 et 404
Pour les pages 403 et 404, je voulais une page plus intégrée dans l'interface du site, et notamment qui diffère suivant le site utilisé, puisque mon instance de Wagtail gère des sites avec deux habillages différents.
Une discussion sur StackOverflow[2] pointe vers une solution qui n’est malheureusement pas ou plus fonctionnelle et a apparemment été retirée de la doc de Wagtail. Elle m’a cependant mis sur la voie de la bonne solution.
La première chose à faire est d’indiquer à Django les vues qui doivent gérer les erreurs 403 et 404 :
# config/urls.py # [...] handler403 = "portfolio.views.custom_403_view" handler404 = "portfolio.views.custom_404_view"
(J’ai décidé de placer ces vues dans l’app portfolio, étant donné que c’est celle qui a le thème différent des autres. Fondamentalement, cela aurait marché dans n’importe quelle app.)
Il faut ensuite créer les vues en question. Comme elles font essentiellement la même chose à part le code d’erreur, on va définir la logique dans une méthode que les deux vues appelleront :
# 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)
Par rapport à la solution proposée sur StackOverflow, on voit quelques changements:
- je vérifie que le site portfolio existe bien, ce qui est notamment utile pour les tests unitaires.
- je fais la comparaison avec
request.environ.get("HTTP_HOST", "")
et non avecrequest.site.hostname
, cette dernière variable n’étant pas disponible dans une page d’erreur, ce qui faisait que le site renvoyait une erreur 500 (sans moyen de voir le problème de fond puisque rien de tout cela n’est accessible quandDEBUG
est activé), y compris sur des pages qui auraient dû marcher (par exemple l’accueil lorsqu’aucune langue n’est sélectionnée.)
Erreurs 400 et 502/503/504
Pour l’erreur 400, j’ai fait la même chose que pour l’erreur 500, mais, comme le plus souvent, lorsqu’elle survient, cela n’arrive même pas à Django, j’ai fait en sorte que Nginx puisse également la servir. Dans le même esprit, j’ai fait une page qui couvre les erreurs 502 (Bad Gateway), 503 (Service Unavailable) et 504 (Gateway Time-out), susceptibles de survenir si Nginx tourne mais que Django ou Gunicorn ne répondent pas. Il s’agit des lignes suivantes de la configuration Nginx :
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
Le dernier bloc permet de tester que la page d’erreur est bien rendue. Pour la 400, un simple caractère '%' à la fin de l’URL suffit à la provoquer.
Page de connexion
Au passage, j’en ai profité pour personnaliser la page de connexion de Wagtail. Cela se fait en créant une page dashboard/templates/wagtailadmin/login.html
et en redéfinissant les blocs pertinents :
{% 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 %}
(Et, oui, j'ai mis les messages d’avertissement de longue durée sur les déchets nucléaires dans une balise <marquee>
et non, je n’ai pas honte :D – par contre j'ai tenté de reproduire le fonctionnement de <marquee>
avec du CSS non considéré comme obsolète par le W3C et je n'ai pas réussi...)
Image d’en-tête:
Un python vert de la Ménagerie du Jardin des Plantes (travail personnel, CC-BY-SA 4.0)