feat(obs): nommer chaque requete, et rendre les pages d erreur audibles

OBS-005. Un 500 dans errors.log et les six lignes de app.log qui y menent
n etaient relies que par leur horodatage — ce qui n est pas une relation
des que le serveur traite plus d une requete a la fois. Et un utilisateur
qui dit « ca a plante quand j ai clique sur enregistrer » ne donnait a
personne de quoi chercher.

Chaque requete recoit un identifiant, porte par toutes les lignes de
journal qu elle produit, renvoye en X-Request-Id, et affiche sur la page
500 comme reference a citer.

Il est **genere**, jamais lu depuis un en-tete entrant. Accepter celui du
client serait pratique pour tracer a travers nginx, et permettrait aussi a
n importe qui d ecrire du texte arbitraire — retours a la ligne compris —
dans le fichier de journal. C est ainsi qu un journal cesse d etre une
preuve. Il n y a de toute facon aucun proxy de confiance tant qu OPS-002
est ouvert.

Le test correspondant assure sur l alphabet plutot qu en envoyant un
retour a la ligne : le client de test de Werkzeug refuse d emettre un tel
en-tete, donc l attaque ne peut meme pas etre construite par la, ce qui ne
prouverait rien sur l application.

**Defaut trouve en chemin, et repare.** Les cinq gabarits d erreur
remplissent le bloc `content`, qui n existait que dans la branche
authentifiee de la mise en page. Un visiteur deconnecte tombant sur une
erreur — donc typiquement sur la page de connexion — recevait le logo, le
selecteur de langue, et **aucun message**. Le code de statut etait bon, les
journaux etaient bons, la page etait vide. Le <title> disait quand meme
« 404 », ce qui explique en grande partie que personne ne l ait vu.

Le bloc est desormais rendu dans les deux branches via self.content(),
Jinja refusant deux blocs de meme nom. Un seul cote du if s execute, donc
jamais de double rendu — et c est assure, pas suppose.

550 tests.
This commit is contained in:
GGThed
2026-08-11 15:30:05 -04:00
parent 7b9eee4805
commit 70db8a7491
9 changed files with 363 additions and 51 deletions
+6
View File
@@ -8,6 +8,12 @@
</div>
<h2>{{ _('500 — Internal Server Error') }}</h2>
<p>{{ _('Something went wrong on our end. The error has been logged and will be investigated. Please try again later.') }}</p>
{# The reference is what makes a report actionable: it names one request
in errors.log. It identifies nothing else — no session, no account —
so there is nothing to protect here (OBS-005). #}
{% if request_id and request_id != '-' %}
<p class="text-muted small">{{ _('Reference to quote if you report this:') }} <code>{{ request_id }}</code></p>
{% endif %}
<a href="{{ url_for('main.dashboard') if current_user.is_authenticated else url_for('auth.login') }}" class="btn btn-primary">
<i class="fas fa-redo-alt"></i> {{ _('Try Again') }}
</a>
+14
View File
@@ -201,6 +201,20 @@
<div class="auth-language">
{% include "layouts/_language_switcher.html" %}
</div>
{# The same `content` block as the signed-in branch, rendered
here too — `self.content()` rather than a second
`{% block %}`, which Jinja refuses.
The error pages (400, 403, 404, 429, 500) all fill `content`,
and it existed only inside the `is_authenticated` branch: a
signed-out visitor hitting any of them got the logo, the
language switcher and no message whatsoever. The <title> still
said "404", which is most of why nobody noticed.
Only one branch of the `if` runs, so this never double-renders.
Sign-in pages fill `auth_content` instead and leave this
empty. #}
{{ self.content() }}
{% block auth_content %}{% endblock %}
</div>
</div>