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 : - 401 et403 ne sont pas interchangeables. Renvoyer403 à un appelant non authentifié peut révéler qu’une ressource existe. - Un 429 sansRetry-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 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" } 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" } 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")) { // ... } 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 : - Intercepter l’erreur à la frontière de l’application. - Journaliser l’exception complète côté serveur avec un identifiant de requête. - 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" } 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 et504 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 } 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 : - Acceptez X-Request-Id lorsqu’il est fourni ou générez-en un. - Ajoutez-le à chaque ligne de journal. - 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 ign
Comments
No comments yet. Start the discussion.