Erreurs & internationalisation¶
Le flux d'erreur traverse trois éléments : les classes d'erreur de domaine (domain/errors.ts), les filtres d'exception HTTP (web/exception.http-filter.ts) qui les traduisent en réponses, et le service i18n (app/i18n/) qui localise les messages.
Règle d'or¶
Toujours lever une erreur de domaine
Les services lèvent des classes d'erreur de domaine — jamais throw new Error('...'), qui court-circuite le filtre HTTP et produit un 500.
Classes d'erreur de domaine¶
| Classe | Statut HTTP | Charge utile |
|---|---|---|
ValidationError |
400 |
{ [champ]: string[] } |
AuthenticationError |
401 |
{ message, code, workspaces? } |
AuthorizationError |
403 |
{ message } |
NotFoundError |
404 |
{ message } |
ConflictError |
409 |
{ message, code? } |
RateLimitError |
429 |
{ message } + en-tête Retry-After |
NotImplementedError |
501 |
{ message } |
PaymentGatewayError |
503 |
{ message } |
ProviderCredentialsRejectedError |
400 |
{ detail: string[] } — aucun champ n'est en cause |
ProviderUnreachableError |
503 |
{ message } |
WebhookSignatureError |
401 |
{ message: 'invalid_signature' }, corps fixe non localisé |
LedgerError |
500 |
{ message } générique (voir ci-dessous) |
ExchangeRateUnavailableError |
500 |
{ message } générique |
PegDeviationError |
500 |
{ message } générique |
Les erreurs Zod (ZodError) sont également mappées en 400, sous le même format dictionnaire que ValidationError.
Deux formes, aucune exception : 400 porte un dictionnaire de champs (clé detail quand le problème est global), tout autre statut porte { message }. Le contrat exposé au frontend est détaillé dans Réponses API.
Les erreurs non filtrées le sont volontairement
LedgerError protège un invariant de comptabilité en partie double construit côté service, pas une entrée client ; ExchangeRateUnavailableError et PegDeviationError signalent un taux manquant ou dérivé de sa parité. Aucune n'a de filtre @Catch : elles résolvent en 500 — corps générique, détail jamais exposé — et restent visibles pour le suivi d'erreurs (Sentry). Ne les repliez jamais dans ValidationError (400), ce qui ferait passer un bug de notre côté pour une erreur utilisateur et éteindrait l'alerte.
Le code machine de AuthenticationError¶
AuthenticationError porte un code machine-lisible que le frontend utilise pour brancher sa logique (rafraîchir le token, proposer un sélecteur de workspace, écran d'activation…) :
invalid_credentials | missing_authorization | workspace_header_required
| no_membership | account_not_activated | user_suspended
Pour workspace_header_required, l'erreur transporte aussi workspaceOptions → la liste workspaces renvoyée au client. Détail complet et tableau des actions frontend : Réponses API.
Localisation (i18n)¶
Tous les messages sont localisés par le backend : le champ message (et les messages de validation) contient du texte prêt à afficher, traduit selon :
- la langue du compte (
user.language) si la requête est authentifiée ; - sinon l'en-tête
Accept-Language; - sinon la langue par défaut.
Les classes d'erreur transportent une clé de message (messageKey, ex. errors.common.not_found) et d'éventuels params, pas du texte figé. Le filtre HTTP résout la clé en texte localisé au moment de la réponse.
| Élément | Emplacement |
|---|---|
| Service i18n | app/i18n/i18n.service.ts |
| Locales (FR/EN) | app/i18n/locales/ |
| Mapping d'erreurs Zod | app/i18n/zod-error-map.ts (clés i18n pour la validation) |
| Résolution de locale | app/i18n/locale.ts |
La traduction est entièrement à notre charge
Le frontend affiche le texte reçu tel quel : il ne re-traduit rien, ne maintient aucune table de clés, et n'a aucun repli à prévoir si une clé manque — c'est un bug backend, corrigé ici. Toute réponse partant d'un filtre doit donc être passée par i18n.t, y compris les messages produits par une dépendance (multer, routeur Nest). Pour brancher un comportement, le client s'appuie sur le status code et, quand il existe, le champ code — jamais sur le texte.
Filtres et interception globale¶
Les filtres d'exception sont enregistrés dans web/app.module.ts (APP_FILTER), aux côtés du filtre global Sentry et de l'intercepteur de contexte. Voir Réponses API pour le contrat HTTP complet exposé au frontend.