Les agents d’IA lisent désormais les erreurs de votre API
En mars, Mintlify a additionné un mois de trafic sur les sites de documentation qu’elle héberge. Cela faisait environ 790 millions de requêtes, dont 45,3 % venaient d’agents de programmation IA. Les navigateurs en faisaient 45,8 %. Claude Code à lui seul envoyait un quart du total (les chiffres de Mintlify).
C’est du trafic vers la documentation, pas vers les API. Mais les agents qui lisent la documentation sont les mêmes qui écrivent ensuite la requête, la lancent et lisent ce qui revient. Dans l’enquête State of the API 2025 de Postman, 24 % des développeurs disaient déjà concevoir leurs API en pensant aux agents. À sa sortie, cela semblait prématuré. Plus maintenant.
L’essentiel de ce que nous construisons ici tourne autour de l’échec : des points d’accès qui renvoient des 500, se bloquent ou s’arrêtent au milieu d’une réponse. Notre question était donc précise. Quand un agent reçoit une erreur de votre API, a-t-il de quoi faire ce qu’il faut ensuite ?
Un agent prend votre message d’erreur au pied de la lettre
Un développeur qui reçoit {"error":"E4021"} cherche le code, ne trouve rien et demande à un collègue. Un agent ne peut demander à personne. Il travaille avec le corps que vous avez envoyé, et si ce corps ne dit rien d’utile, il devine : il relance la même requête ou modifie un champ qui était correct.
Voici ce que dit notre point d’accès produits quand l’ID n’existe pas :
$ curl -i https://api.testingapis.com/api/Products/99999
HTTP/1.1 404 Not Found
content-type: application/json; charset=utf-8
{"status":404,"error":"Not Found","message":"Product with ID 99999 does not exist."}C’est une phrase banale, et c’est très bien. Elle nomme la ressource et l’ID, donc celui qui la lit, humain ou programme, sait que l’ID est le problème et que renvoyer la requête ne servira à rien.
Si vous voulez une forme standard pour les erreurs, la RFC 9457 (Problem Details for HTTP APIs) définit type, title, status, detail et instance, servis en application/problem+json. Elle a remplacé la RFC 7807 en 2023. Nous ne pensons pas que le format exact compte beaucoup. Ce qui compte, c’est qu’il y ait quelque part une phrase claire.
Dites combien de temps attendre
Les agents réessaient. Si votre 429 ou votre 503 ne dit pas combien de temps patienter, le client doit choisir un nombre, et un agent qui traite une tâche en boucle continuera de choisir. Retry-After fait partie de HTTP depuis des décennies et règle cela avec un seul en-tête :
$ curl -i https://api.testingapis.com/api/status/429
HTTP/1.1 429 Too Many Requests
content-type: application/json; charset=utf-8
retry-after: 5
{"status":429,"statusText":"Too Many Requests","category":"client-error","message":"This is a 429 response. Use /api/status/{code} to request any status."}/api/status/429L’IETF travaille aussi sur les en-têtes RateLimit et RateLimit-Policy, qui permettent à un client de voir son quota avant de l’épuiser plutôt qu’après. Le brouillon 11 est sorti en mai. La syntaxe ressemble à ceci :
RateLimit-Policy: "default";q=100;w=60
RateLimit: "default";r=50;t=30Cela se lit "100 requêtes par 60 secondes, il en reste 50, la fenêtre se réinitialise dans 30". C’est encore un brouillon, donc nous ne retirerions pas les en-têtes X-RateLimit-* que vos clients lisent déjà. Envoyer les deux coûte très peu.
Les nouvelles tentatives créent des doublons
Relancer un GET est sans danger. Relancer un POST ne l’est pas, si la première tentative a atteint le serveur et que seule la réponse s’est perdue. Vous vous retrouvez avec deux commandes. Un humain repère en général le double débit. Un agent voit que sa deuxième tentative a marché et passe à la suite.
La solution habituelle est une clé d’idempotence. Le client envoie un en-tête Idempotency-Key unique avec l’écriture, et si la même clé revient, le serveur renvoie le premier résultat au lieu de refaire le travail. Stripe fonctionne ainsi depuis des années, et il existe un brouillon de l’IETF pour l’en-tête, même s’il n’est pas encore une RFC. Si votre API encaisse des paiements ou crée quoi que ce soit de pénible à nettoyer, cela vaut la peine de l’ajouter avant que les agents ne l’appellent.
Les serveurs MCP deviennent des API HTTP ordinaires
Beaucoup d’agents n’appelleront pas directement votre API REST. Ils passeront par un serveur MCP qui l’enveloppe. La spécification MCP publiée le 28 juillet a supprimé la poignée de main de session et l’en-tête Mcp-Session-Id. Chaque requête porte désormais sa propre version du protocole et les informations du client, et les noms de méthode et d’outil voyagent dans des en-têtes HTTP, si bien qu’une passerelle peut router et limiter les requêtes sans analyser le corps.
Résultat : un serveur MCP se comporte maintenant comme n’importe quel autre service HTTP sans état. Il peut se placer derrière un répartiteur de charge ordinaire, et les conseils ci-dessus s’appliquent tels quels. Quand un appel d’outil échoue, l’agent a besoin d’une raison et d’un délai d’attente de la part de votre serveur MCP, comme de votre API.
Une vérification rapide
Voici le script que nous avons lancé en écrivant ces lignes. Il envoie quatre requêtes qui échouent et vérifie deux choses pour chacune : y a-t-il une raison lisible par un programme, et y a-t-il un Retry-After ?
// For each failure: is there a reason a program can read, and a time to wait?
const BASE = "https://api.testingapis.com";
const cases = [
["missing product", "/api/Products/99999"],
["rate limited", "/api/status/429"],
["server down", "/api/Products/1?error=503"],
["bad request", "/api/status/400"],
];
for (const [name, path] of cases) {
const res = await fetch(BASE + path);
const body = await res.json().catch(() => ({}));
const reason = body.message ?? body.detail ?? body.title;
const wait = res.headers.get("retry-after");
console.log(
name.padEnd(16),
res.status,
reason ? "reason: yes" : "reason: NO ",
wait ? `retry-after: ${wait}s` : "retry-after: -",
);
}missing product 404 reason: yes retry-after: -
rate limited 429 reason: yes retry-after: 5s
server down 503 reason: yes retry-after: -
bad request 400 reason: yes retry-after: -Chaque réponse a une raison, c’est bien. Mais regardez la troisième ligne. /api/status/503 envoie Retry-After: 5, et ?error=503 sur un vrai point d’accès n’envoie rien. Un client testé contre lui n’apprendrait jamais quand réessayer. Celui-là est sur notre liste.
Pointez le même script vers les chemins d’échec de votre propre API. Si vous ne pouvez pas faire échouer votre API à la demande, le guide des réponses d’erreur montre comment obtenir de la nôtre un 500, un 429, un timeout ou un corps tronqué.
Rien de tout cela n’est nouveau. Des messages d’erreur clairs et Retry-After étaient de bonnes idées bien avant que quiconque construise un agent. Ce qui a changé, c’est qu’un humain perdu finit par vous écrire un e-mail, et qu’un agent, lui, réessaie simplement.