6. Échecs lisibles par machine¶
Règle. Une erreur côté serveur produit toujours :
{"success": false, "code": "<stable>", "message": "<human>", "details": {...}}. Le client se cale surcode, JAMAIS surmessage.
Pourquoi¶
Une chaîne "User not found" est :
- Dépendante de la locale (l'i18n change la chaîne).
- Mutable pour raisons cosmétiques (un dev change "User" en "Profile" pendant un refactor).
- Non structurée (impossible d'extraire l'identifiant concerné).
Conséquence : le client gère mal les erreurs, ou pire, écrit des regex fragiles sur le message.
L'enveloppe d'erreur canonique¶
Toute réponse 4xx ou 5xx retourne :
{
"success": false,
"code": "mail.invalid_recipient",
"message": "recipient must be a non-empty email address",
"details": {
"received_recipient": " "
}
}
| Champ | Type | Stable ? | Usage côté client |
|---|---|---|---|
success |
false |
toujours false ici |
discriminant |
code |
str |
oui (versionné) | if code === "mail.invalid_recipient" |
message |
str |
non | affichage de repli / débogage |
details |
dict \| null |
structure stable par code | extraire des données contextualisées |
Conventions de nommage des codes¶
<domain>.<verb> ou <domain>.<noun> :
| Code | Signification |
|---|---|
users.not_found |
Ressource absente |
users.invalid_email |
Validation métier |
auth.unauthorized |
Token absent / malformé |
auth.forbidden |
Token valide mais pas le bon scope |
transient.database_unavailable |
DB qui ne répond pas (mappé 503) |
transient.upstream_down |
Service amont KO (mappé 503) |
http.validation_error |
Pydantic ValidationError |
Convention : transient.* pour ce qui justifie un retry côté client,
<domain>.* pour ce qui ne le justifie pas (correction métier nécessaire).
Les erreurs sont listables¶
Chaque module documente les codes qu'il émet :
"""Module audit — service d'audit log centralisé.
Codes d'erreur émis :
- ``audit.invalid_actor`` : actor vide
- ``audit.invalid_action`` : action vide
- ``audit.invalid_limit`` : limit hors [1, 1000]
- ``audit.invalid_offset`` : offset < 0
- ``audit.not_found`` : event_id inconnu
"""
Pour un catalogue centralisé multi-services, voir
CATALOG_ERREURS.md (sentinel).
Pourquoi success: false plutôt que regarder le statut HTTP ?¶
Trois raisons :
- Lisibilité dans les logs JSON. Un
{"success": false, "code": "mail.not_found"}cherche encode:"mail.not_found". Sans le discriminant explicite, il faut lire le status code dans une autre colonne. - Robustesse aux proxies. Un proxy qui réécrit le statut (CDN, WAF, répartiteur de charge mal configuré) ne réécrit pas le corps. Le client garde un signal fiable.
- Cohérence avec les succès. L'enveloppe de succès
(
{"success": true, "data": ...}) a déjà un discriminant. L'erreur en a aussi. Le client a une seule règle d'analyse.
Test miroir : le code est vérifié¶
Tous les codes d'erreur sont vérifiés en test miroir :
def test_enqueue_invalid_recipient_returns_400(client):
response = client.post("/api/v1/messages", json={"recipient": "bad", ...})
assert response.status_code == 400
assert response.json()["code"] == "mail.invalid_recipient"
Si quelqu'un renomme le code par inadvertance, le test casse — la contrainte est encodée dans le code.