DEV Community

Cover image for Meilleures pratiques de gestion des erreurs des API REST : Codes d'état, RFC 9457 et erreurs réessayables
Antoine Laurent
Antoine Laurent

Posted on Originally published at apidog.com

Meilleures pratiques de gestion des erreurs des API REST : Codes d'état, RFC 9457 et erreurs réessayables

Concevoir un contrat d’erreur robuste pour une API REST

Les réponses d’erreur de votre API font partie de son contrat. Les clients les analysent, les mécanismes de réessai s’appuient sur elles et les équipes support les consultent à 2 heures du matin. Pourtant, beaucoup d’équipes détaillent le chemin nominal et laissent les erreurs dépendre des valeurs par défaut du framework. Résultat : plusieurs formats d’erreur dans une même API, une réponse 200 contenant success: false ou encore une trace de pile qui divulgue le schéma de la base de données.

Essayez Apidog dès aujourd'hui

Ce guide présente une approche de bout en bout : choisir le bon code HTTP, standardiser le corps avec les Problem Details de la RFC 9457, séparer les codes machine des messages humains, indiquer si une erreur peut être réessayée et empêcher toute fuite d’informations sensibles. Il montre également comment tester chaque chemin d’échec dans Apidog.

Commencez par le code d’état, pas par le corps

HTTP fournit déjà une première couche de sémantique. La RFC 9110 définit notamment les familles suivantes :

  • 4xx : le client a envoyé une requête incorrecte ; la même requête échouera généralement à nouveau.
  • 5xx : le serveur ou une dépendance a échoué ; la requête du client peut être valide.

Les clients génériques, les proxies, les caches et les bibliothèques de réessai utilisent ces codes sans lire votre JSON. Consultez la référence MDN des codes d’état HTTP et appliquez une convention cohérente.

Situation Utiliser Ne pas utiliser Pourquoi
JSON invalide, type de contenu incorrect ou champ obligatoire manquant 400 Bad Request 422 Le serveur ne peut pas analyser ou comprendre la requête.
Requête valide mais règles métier violées : montant négatif, devise non supportée 422 Unprocessable Content 400 La syntaxe est correcte, mais les valeurs ne le sont pas.
Identifiants absents, expirés ou invalides 401 Unauthorized 403 Le client n’a pas prouvé son identité. Envoyez WWW-Authenticate.
Identifiants valides mais permissions insuffisantes 403 Forbidden 401 L’identité est connue, mais l’accès est refusé.
Ressource inexistante ou existence à ne pas confirmer 404 Not Found 410 Le comportement par défaut masque aussi les ressources aux sondages non autorisés.
Ressource supprimée définitivement 410 Gone 404 Indique aux clients et aux robots de supprimer leurs références.
Conflit d’état : doublon, version périmée, collision d’édition 409 Conflict 400 La requête est valide mais incompatible avec l’état actuel.
Limite de débit dépassée 429 Too Many Requests 503 Ajoutez toujours Retry-After.
Exception non gérée dans votre code 500 Internal Server Error 502 Votre serveur a échoué.
Réponse invalide d’un service en amont 502 Bad Gateway 500 L’échec se situe derrière votre passerelle.
Serveur surchargé ou en maintenance 503 Service Unavailable 500 L’échec est temporaire ; ajoutez Retry-After si possible.
Service en amont expiré 504 Gateway Timeout 500 Distingue une dépendance lente d’un code défectueux.

Deux distinctions sont particulièrement importantes :

  1. 401 et 403 ne sont pas interchangeables. Renvoyer 403 à un appelant non authentifié peut révéler qu’une ressource existe.
  2. Un 429 sans Retry-After encourage les clients à réessayer immédiatement en boucle. Le guide sur la limitation de débit d’une API détaille les algorithmes et les en-têtes associés.

Utilisez une seule forme : RFC 9457 Problem Details

Une fois le code d’état choisi, toutes les erreurs devraient utiliser le même type de média et le même schéma :

Content-Type: application/problem+json
Enter fullscreen mode Exit fullscreen mode

La RFC 9457 définit cinq membres principaux :

  • type : URI identifiant la catégorie d’erreur ;
  • title : résumé court destiné aux humains ;
  • status : code HTTP, répété par commodité ;
  • detail : description de cette occurrence ;
  • instance : URI identifiant l’échec précis.

Les champs supplémentaires sont des membres d’extension. La présentation détaillée de la RFC 9457 explique notamment comment elle remplace la RFC 7807.

Voici une réponse de validation pour un point de terminaison de paiement :

POST /v1/payments HTTP/1.1
Content-Type: application/json

{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }
Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "La validation de la requête a échoué",
  "status": 422,
  "detail": "Un ou plusieurs champs n'ont pas passé la validation.",
  "instance": "/v1/payments/requests/req_9f3c1a7b",
  "code": "PAYMENT_VALIDATION_FAILED",
  "errors": [
    {
      "field": "amount",
      "code": "AMOUNT_NOT_POSITIVE",
      "message": "le montant doit être un entier positif en unités mineures"
    }
  ],
  "request_id": "req_9f3c1a7b"
}
Enter fullscreen mode Exit fullscreen mode

errors[] est un membre d’extension particulièrement utile : une interface peut associer chaque erreur au champ exact du formulaire au lieu d’afficher une bannière générique.

Choisissez un format stable pour les chemins, comme JSON Pointer ou une notation pointée, puis utilisez-le partout.

Appliquez cette structure à toutes les erreurs, y compris celles produites par un framework ou une passerelle. Si vos gestionnaires renvoient des Problem Details mais qu’un équilibreur de charge renvoie du HTML pour une erreur 502, les clients devront maintenir deux parseurs.

Séparez les codes machine des messages humains

L’exemple contient volontairement un code et un message. Ces champs répondent à des besoins différents.

Les codes machine, comme AMOUNT_NOT_POSITIVE, CURRENCY_UNSUPPORTED ou IDEMPOTENCY_KEY_REUSED, font partie du contrat. Ils doivent être :

  • stables ;
  • documentés ;
  • énumérables.

Les clients ne doivent jamais analyser le texte d’un message :

// À éviter
if (error.message.includes("positif")) {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Une simple correction éditoriale deviendrait alors un changement cassant.

Les messages humains peuvent évoluer librement. Ils doivent expliquer l’échec et indiquer comment le corriger :

« le montant doit être un entier positif en unités mineures »

est plus utile que :

« montant invalide »

Si vous localisez les messages, gardez les codes inchangés.

Cette séparation est également essentielle pour les consommateurs pilotés par des agents autonomes. Les clients basés sur des LLM se rétablissent mieux avec des erreurs structurées et auto-descriptives ; voir la conception de messages d’erreur pour les agents IA.

Ne divulguez jamais ces informations

Les erreurs non gérées constituent un canal de reconnaissance privilégié. Votre middleware doit empêcher les éléments suivants d’atteindre le client :

  • traces de pile, noms de classes et chemins de fichiers ;
  • SQL brut, fragments de requête et erreurs ORM ;
  • noms d’hôtes internes, adresses IP, ports et noms de services ;
  • versions de bibliothèques et signatures de frameworks ;
  • secrets, jetons et chaînes de connexion présents dans une exception ;
  • existence d’un compte utilisateur, notamment lors de la connexion ou de la réinitialisation d’un mot de passe.

Le modèle recommandé est simple :

  1. Intercepter l’erreur à la frontière de l’application.
  2. Journaliser l’exception complète côté serveur avec un identifiant de requête.
  3. Renvoyer un Problem Details générique contenant ce même identifiant.
{
  "type": "https://api.example.com/problems/internal-error",
  "title": "Erreur interne",
  "status": 500,
  "detail": "Une erreur interne est survenue",
  "request_id": "req_51ad0"
}
Enter fullscreen mode Exit fullscreen mode

Le client reçoit une information sûre, tandis que les journaux conservent les détails nécessaires au diagnostic.

Indiquez si l’erreur peut être réessayée

Chaque erreur répond à une question importante : « dois-je réessayer ? » Encodez cette décision dans le contrat plutôt que de laisser chaque équipe cliente deviner.

Par défaut :

  • 429, 502, 503 et 504 peuvent être réessayés avec retrait exponentiel et gigue ;
  • 500 peut faire l’objet d’un réessai prudent ;
  • la plupart des autres 4xx sont terminaux.

Réessayer un 401, 403, 404 ou 422 avec la même requête gaspille du quota et pollue les journaux.

Les délais d’attente nécessitent une attention particulière. Une requête peut avoir abouti après l’abandon du client. Pour les opérations mutables, utilisez donc des clés d’idempotence afin qu’un paiement réessayé ne soit pas facturé deux fois.

Vous pouvez rendre la décision explicite :

{
  "type": "https://api.example.com/problems/rate-limited",
  "title": "Trop de requêtes",
  "status": 429,
  "code": "RATE_LIMITED",
  "retryable": true,
  "retry_after_seconds": 30
}
Enter fullscreen mode Exit fullscreen mode

Le champ retryable permet de remplacer les valeurs par défaut. Par exemple, un sous-code 500 peut être marqué comme terminal s’il existe un risque de corruption d’état. Documentez cette règle une seule fois pour obtenir un comportement uniforme dans vos SDK.

Ajoutez un identifiant de corrélation

Attribuez un identifiant à chaque requête :

  1. Acceptez X-Request-Id lorsqu’il est fourni ou générez-en un.
  2. Ajoutez-le à chaque ligne de journal.
  3. Renvoyez-le dans chaque réponse d’erreur sous le nom request_id.

Quand un client joint une erreur à un ticket support, cet identifiant transforme une longue recherche dans les journaux en une requête ciblée.

Dans un environnement distribué, propagez également traceparent au format W3C afin de suivre la requête entre les services.

Versionnez le contrat d’erreur

Traitez le contrat d’erreur comme le reste de votre API.

En général, ces changements sont compatibles :

  • ajouter un membre d’extension ;
  • ajouter un nouveau code d’erreur.

En revanche, ces changements sont cassants :

  • renommer errors[].field ;
  • modifier la signification d’un code existant ;
  • remplacer une structure ad hoc par Problem Details.

L’URI type fournit un mécanisme de versionnement propre :

  • conservez les anciens URI type de manière stable ;
  • introduisez de nouveaux URI pour de nouvelles sémantiques ;
  • documentez que les membres d’extension et les codes inconnus doivent être ignorés.

Cette règle de compatibilité ascendante permet de faire évoluer l’API sans créer immédiatement une v2.

Testez chaque chemin d’erreur dans Apidog

Les contrats d’erreur se dégradent lorsqu’ils ne sont jamais exécutés. Le chemin nominal apparaît à chaque démonstration ; une branche 422 n’est souvent déclenchée qu’en production.

Faites donc des scénarios d’échec des citoyens de première classe dans votre suite de tests.

Scénarios côté serveur

Pour chaque point de terminaison, créez au minimum un scénario par cas d’échec :

  • authentification absente → 401 ;
  • rôle insuffisant → 403 ;
  • montant négatif → 422 avec errors[0].code = AMOUNT_NOT_POSITIVE ;
  • trafic excessif → 429 avec l’en-tête Retry-After.

Les assertions d’Apidog vérifient l’état, les en-têtes et les champs du corps sans script. Vous pouvez aussi valider toute la charge utile contre votre schéma JSON Problem Details. Un écart fait alors échouer la CI plutôt que la production.

Consultez le guide des assertions API pour les modèles disponibles.

Serveurs de maquette côté client

Les équipes front-end et SDK doivent pouvoir développer les réponses 4xx et 5xx avant que le back-end ne les produise réellement.

Les serveurs de maquette Apidog renvoient les corps Problem Details exacts de votre spécification API. Vous pouvez ainsi simuler :

  • un 503 avec Retry-After: 120 ;
  • un 409 après une double soumission ;
  • une erreur de validation complète avec errors[].

Observez ensuite l’affichage et le comportement de réessai du client, sans stub Express manuel ni code back-end temporairement commenté.

Concevez le contrat, encodez-le en scénarios et en maquettes, puis exécutez les deux dans la CI. Vous pouvez télécharger Apidog et importer une spécification OpenAPI existante pour obtenir rapidement des réponses d’erreur simulables.

FAQ

Dois-je utiliser 400 ou 422 pour les erreurs de validation ?

Utilisez 400 lorsque la requête est mal formée et incompréhensible : JSON invalide, type de contenu incorrect ou champ obligatoire manquant.

Utilisez 422 lorsque la requête est correctement analysée mais que ses valeurs enfreignent les règles métier, par exemple un montant négatif ou une devise non supportée.

Un 422 signifie « corrigez vos données », tandis qu’un 400 signifie « corrigez le format de la requête ». L’essentiel est d’appliquer la même convention à tous les points de terminaison.

Qu’est-ce que application/problem+json ?

C’est le type de média défini par la RFC 9457 pour les Problem Details, le format JSON standard des erreurs HTTP. Il contient notamment type, title, status, detail et instance, ainsi que vos extensions comme errors[].

Ce type de média permet aux clients génériques et aux middlewares de reconnaître vos erreurs sans configuration personnalisée.

Quelles erreurs HTTP les clients doivent-ils réessayer automatiquement ?

Réessayez 429, 502, 503 et 504 avec un retrait exponentiel et une gigue, en respectant Retry-After lorsqu’il est présent. Traitez 500 comme potentiellement réessayable, mais avec prudence.

Ne réessayez pas les autres réponses 4xx sans modifier la requête. Pour les opérations mutables, utilisez une clé d’idempotence afin qu’une requête rejouée ne puisse pas facturer ou créer deux fois.

Comment tester les réponses d’erreur sans casser le back-end ?

Simulez-les avec un serveur de maquette Apidog qui renvoie les corps 4xx et 5xx définis dans votre spécification. Vérifiez ensuite le rendu et le comportement de réessai du client.

Côté serveur, envoyez des charges invalides, une authentification absente et suffisamment de trafic pour déclencher la limitation, puis validez les codes, les en-têtes et le schéma du corps. Exécutez ces tests dans la CI pour maintenir un contrat d’erreur fiable.

Top comments (1)

Collapse
 
crdtcto profile image
Kane Lim

Hello Glad to see you, I am Kane Lim from Hong Kong. I have over 10 years of development experience. I am writing this because your post was interesting.

The strongest part of this architecture is treating error responses as a versioned API contract rather than framework output. I would push this further by making error semantics executable across the entire platform.

I prefer a centralized error taxonomy where each domain error maps deterministically to HTTP status, Problem Details type, machine code, retry policy, observability severity, and remediation strategy. This prevents different microservices from independently deciding whether the same failure is retryable.

For distributed systems, I would also propagate traceparent and request_id through every service boundary, while attaching structured error metadata to OpenTelemetry spans. Then a single client failure can be correlated across gateway, service, database and downstream dependency without searching logs manually.

Another important layer is contract testing. Generate negative test cases directly from the OpenAPI specification and assert status, content type, required Problem Details fields, headers and machine codes in CI. Consumer driven contract tests can additionally verify that SDKs never depend on localized detail messages.

For retryable failures, I would avoid relying solely on retryable=true. Combine error classification with an operation deadline, retry budget and idempotency policy. That prevents nested clients from multiplying retries during dependency degradation.

This approach turns error handling from defensive coding into an observable, testable reliability protocol. Excellent writeup, Antoine. I would enjoy exchanging ideas on API architecture and distributed systems with you.