Aller au contenu

Gestion des erreurs API (frontend)

Ce document décrit tous les formats de réponse d'erreur (statuts 4xx / 5xx) que le backend ikloze peut renvoyer, et comment le frontend doit les traiter.

Source de vérité : web/exception.http-filter.ts (les filtres d'exception) et domain/errors.ts (les classes d'erreur). Toute divergence entre ce document et ces fichiers est un bug de doc — c'est le code qui fait foi.


Principes généraux

  1. Le corps d'une erreur est toujours du JSON. Content-Type: application/json.
  2. Tous les messages sont déjà localisés par le backend. Le champ message (et les messages de validation) contient du texte prêt à afficher, traduit selon :
  3. la langue du compte utilisateur (user.language) si la requête est authentifiée ;
  4. sinon l'en-tête Accept-Language de la requête ;
  5. sinon la langue par défaut.
  6. 👉 Le frontend ne doit pas re-traduire ni mapper ces messages via une table de clés — ils sont affichables tels quels.
  7. Deux familles de format seulement :
  8. 400 → un dictionnaire de champs : { <champ>: string[] }.
  9. Tous les autres → un objet avec au minimum une clé message: string, parfois enrichi de champs additionnels (code, workspaces, …).
  10. Ne jamais se fier au texte du message pour brancher la logique. Pour décider d'un comportement (rafraîchir le token, proposer un workspace, etc.), utiliser le status code et, quand il existe, le champ code.

Tableau récapitulatif

Status Origine Forme du corps Champs additionnels
400 Validation (Zod ou métier) { [champ: string]: string[] }
401 Authentification { message } code, workspaces?
403 Autorisation (permissions) { message }
404 Ressource introuvable { message }
409 Conflit (état / unicité) { message } code?
429 Rate limiting { message } en-tête Retry-After
500 Erreur serveur non gérée { statusCode, message } (format NestJS par défaut)
501 Fonctionnalité non implémentée { message }
503 Passerelle de paiement indispo. { message }

400 Bad Request — erreurs de validation

C'est le seul statut dont le corps n'a pas de clé message. Le corps est un dictionnaire : chaque clé est le nom d'un champ fautif, chaque valeur est un tableau de messages (un champ peut accumuler plusieurs erreurs).

// Erreurs au niveau des champs
{
  "email": ["L'adresse e-mail est invalide."],
  "password": [
    "Le mot de passe doit faire au moins 8 caractères.",
    "Le mot de passe doit contenir un chiffre."
  ]
}

Erreurs « object-level » (qui ne portent pas sur un champ précis)

Quand l'erreur ne concerne aucun champ identifiable (validation croisée, payload globalement invalide), le backend la range sous une clé conventionnelle :

  • detail — clé par défaut pour les erreurs Zod sans chemin de champ.
  • une clé de champ ad hoc choisie par le service (ex. un service peut émettre { target: [...] } pour une transition d'état illégale).
// Erreur globale, non rattachée à un champ du formulaire
{
  "detail": ["Les dates de début et de fin sont incohérentes."]
}

Gestion frontend

  • Itérer sur les clés de l'objet pour positionner les erreurs sous chaque champ de formulaire.
  • Réserver un emplacement d'affichage pour la clé detail (et toute clé inconnue ne correspondant pas à un champ du formulaire) → l'afficher comme erreur de niveau formulaire.
  • Toujours afficher tous les éléments du tableau, pas seulement le premier.
  • Forme TypeScript : type ValidationErrorBody = Record<string, string[]>.

Détection : un 400 est reconnaissable au fait que les valeurs sont des tableaux et qu'il n'y a pas de clé message.


401 Unauthorized — authentification

Émis par les guards d'authentification. Le corps contient message, un code machine-lisible, et optionnellement une liste workspaces.

{
  "message": "Votre session a expiré.",
  "code": "invalid_credentials",
  "workspaces": null            // présent seulement dans le cas workspace_header_required
}

Valeurs possibles de code

code Signification Action frontend recommandée
missing_authorization Aucun token fourni (en-tête Authorization absent). Rediriger vers la page de connexion.
invalid_credentials Identifiants faux ou token invalide / expiré. Tenter un refresh ; si échec, déconnecter et rediriger vers la connexion.
account_not_activated Compte non encore activé (e-mail non confirmé). Afficher l'écran « activez votre compte » / renvoyer l'e-mail d'activation.
user_suspended Compte suspendu. Afficher un écran de compte suspendu ; pas de retry.
workspace_header_required L'utilisateur a plusieurs workspaces et n'en a sélectionné aucun. Afficher un sélecteur de workspace à partir du tableau workspaces, puis renvoyer la requête avec l'en-tête X-Workspace-Id.
no_membership L'utilisateur n'appartient à aucun workspace. Rediriger vers la création/jonction d'un workspace.

Le champ workspaces

Présent (non-null) uniquement avec code: "workspace_header_required". Tableau d'options de workspace parmi lesquels choisir :

{
  "message": "Veuillez sélectionner un espace de travail.",
  "code": "workspace_header_required",
  "workspaces": [
    { "id": "01J9X...", "name": "Acme Closers" },
    { "id": "01J9Y...", "name": "Side Project" }
  ]
}

Le frontend présente ces options, puis rejoue la requête avec l'en-tête X-Workspace-Id: <id choisi>.

Cas particulier (webhooks paiement) : une signature de webhook invalide renvoie 401 avec seulement { "message": "invalid_signature" } — pas de code, pas de workspaces. Ce cas ne concerne pas le client frontend (appel serveur-à-serveur du prestataire de paiement).


403 Forbidden — autorisation

L'utilisateur est authentifié mais n'a pas la permission d'effectuer l'action (RBAC), ou le workspace est suspendu.

{ "message": "Vous n'avez pas la permission d'effectuer cette action." }

Gestion frontend

  • Une seule clé message. Afficher le message ; ne pas déconnecter (l'auth est valide).
  • Idéalement, masquer en amont les actions interdites pour éviter d'atteindre ce cas.

404 Not Found — ressource introuvable

La ressource n'existe pas ou n'est pas visible dans le workspace actif (le backend renvoie volontairement 404 plutôt que 403 pour ne pas divulguer l'existence d'une ressource d'un autre workspace — anti-énumération).

{ "message": "Ressource introuvable." }

Gestion frontend

  • Une seule clé message. Afficher un état « introuvable » ou rediriger vers la liste parente.
  • Ne pas interpréter un 404 comme « accès refusé » dans l'UI : pour l'utilisateur, la ressource n'existe simplement pas.

409 Conflict — conflit d'état ou d'unicité

L'action entre en conflit avec l'état actuel (ressource déjà existante, ligne verrouillée, doublon sur une contrainte d'unicité, transition interdite, …).

// Forme minimale
{ "message": "Un compte existe déjà avec cette adresse e-mail." }

// Forme enrichie (code optionnel)
{
  "message": "Un compte existe déjà avec cette adresse e-mail.",
  "code": "email_taken"
}

Gestion frontend

  • Le champ code est optionnel : ne pas supposer qu'il est présent.
  • Quand code est fourni, l'utiliser pour un traitement ciblé (ex. email_taken → rattacher l'erreur au champ e-mail du formulaire). Sinon, afficher message au niveau formulaire.

429 Too Many Requests — rate limiting

Trop de requêtes sur une fenêtre de temps.

{ "message": "Trop de requêtes. Réessayez plus tard." }
  • En-tête Retry-After (en secondes) présent quand le délai est connu.

Gestion frontend

  • Lire l'en-tête Retry-After pour temporiser un nouvel essai (backoff) et/ou désactiver le bouton pendant ce délai.
  • Afficher message à l'utilisateur.

500 Internal Server Error — erreur serveur non gérée

Toute exception non rattachée à un filtre spécifique (bug, panne d'infrastructure, invariant interne — ex. LedgerError) tombe dans le filtre global Sentry et produit le format d'erreur NestJS par défaut :

{
  "statusCode": 500,
  "message": "Internal server error"
}

Gestion frontend

  • ⚠️ Le format diffère des autres : la clé message existe mais il y a aussi statusCode, et le message n'est pas localisé (texte technique générique).
  • Ne jamais afficher ce message brut à l'utilisateur. Afficher un message d'erreur générique de l'UI (« Une erreur est survenue, réessayez »).
  • Loguer / remonter l'incident (l'erreur est déjà tracée côté backend via Sentry).

501 Not Implemented — fonctionnalité non disponible

Endpoint volontairement non encore implémenté.

{ "message": "Cette fonctionnalité n'est pas encore disponible." }
  • Une seule clé message. Désactiver/masquer la fonctionnalité concernée côté UI.

503 Service Unavailable — passerelle de paiement indisponible

Le prestataire de paiement (Mobile Money / carte) est injoignable ou en erreur.

{ "message": "Le service de paiement est momentanément indisponible." }

Gestion frontend

  • Une seule clé message. Proposer de réessayer plus tard ; ne pas considérer le paiement comme échoué définitivement.
  • Ne pas relancer automatiquement en boucle — laisser l'utilisateur réessayer.

Types TypeScript de référence

// 400 uniquement
type ValidationErrorBody = Record<string, string[]>;

// 401
interface AuthErrorBody {
  message: string;
  code:
    | 'invalid_credentials'
    | 'missing_authorization'
    | 'workspace_header_required'
    | 'no_membership'
    | 'account_not_activated'
    | 'user_suspended';
  workspaces?: { id: string; name: string }[] | null;
}

// 403 / 404 / 429 / 501 / 503
interface MessageErrorBody {
  message: string;
}

// 409
interface ConflictErrorBody {
  message: string;
  code?: string;
}

// 500 (format NestJS, message non localisé)
interface ServerErrorBody {
  statusCode: number;
  message: string;
}

Discriminant de parsing recommandé

function parseApiError(status: number, body: unknown) {
  if (status === 400) {
    // body: Record<string, string[]> — itérer sur les champs
    return { kind: 'validation', fields: body as ValidationErrorBody };
  }
  if (status >= 500) {
    // message générique, ne pas afficher body.message tel quel
    return { kind: 'server' };
  }
  // 401/403/404/409/429/501/503 → toujours une clé `message`
  return { kind: 'app', body: body as MessageErrorBody };
}

Récapitulatif des décisions frontend par statut

  • 400 → mapper { champ: messages[] } sur le formulaire ; gérer la clé detail comme erreur globale.
  • 401 → brancher sur code (refresh, sélection de workspace, activation, etc.), jamais sur le texte.
  • 403 → afficher message, ne pas déconnecter.
  • 404 → état « introuvable », ne pas interpréter comme un refus d'accès.
  • 409 → si code présent, traitement ciblé ; sinon afficher message.
  • 429 → respecter Retry-After, temporiser.
  • 500 → message UI générique, ignorer le message du backend.
  • 501 / 503 → afficher message, proposer de réessayer plus tard.