Aller au contenu

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.

const record = await Workspaces.findByPk(id);
if (!record) throw new NotFoundError();

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 :

  1. la langue du compte (user.language) si la requête est authentifiée ;
  2. sinon l'en-tête Accept-Language ;
  3. 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.