Une API simulée qui renvoie des erreurs à la demande
Obtenez un 500, un 429 avec Retry-After, un timeout ou un corps tronqué dès que vos tests en ont besoin.
Exemples exécutés pour la dernière fois contre l’API le
Pour tester comment votre code gère une API en échec, pointez-le vers un point d’accès qui échoue exprès. https://api.testingapis.com est gratuite, sans clé, et accepte les requêtes de toute origine. /api/status/500 renvoie un 500 et /api/status/429 un 429 avec Retry-After. N’importe quel code de 200 à 599 fonctionne. Ajouter ?error=503 à un vrai point d’accès le fait échouer, et /api/chaos/retry/{key}connectez-vous pour voir échoue un nombre de fois donné avant de réussir. /api/chaos/timeoutconnectez-vous pour voir reste bloqué jusqu’à ce que votre client abandonne, et /api/chaos/partialconnectez-vous pour voir envoie une demi-réponse. Voici chacun d’eux, avec la réponse qu’il renvoie.
Quel point d’accès pour quelle panne
| Vous voulez tester | Requête | Ce qui revient |
|---|---|---|
| Un code de statut précis | GET /api/status/503 | Ce statut, un corps JSON et les en-têtes qu’il porte d’habitude |
| Un vrai point d’accès en échec | GET /api/Products/1?error=500 | Le statut demandé, avec X-Simulated: true |
| Un vrai point d’accès lent | GET /api/Products/1?delay=3 | La réponse normale après N secondes (10 au maximum) |
| La logique de nouvelle tentative | GET /api/chaos/retry/my-key?succeedAfter=2connectez-vous pour voir | Deux échecs, puis 200. X-Attempt compte les tentatives |
| Des échecs aléatoires | GET /api/chaos/flaky?failRate=0.3connectez-vous pour voir | 503 avec Retry-After: 1 environ 30 % du temps |
| Un timeout du client | GET /api/chaos/timeout?seconds=30connectez-vous pour voir | Rien jusqu’à la fin du délai (de 1 à 60 secondes) |
| Un corps tronqué | GET /api/chaos/partialconnectez-vous pour voir | Les en-têtes annoncent 10 000 octets, environ 64 arrivent, puis la connexion se ferme |
| Une vraie limite de débit | GET /api/RateLimitconnectez-vous pour voir | 429 après 5 requêtes en 60 secondes, avec Retry-After |
| Un mélange pondéré | GET /api/status/random?codes=200:3,500:1 | 200 trois fois plus souvent que 500 |
Renvoyer n’importe quel code de statut HTTP
/api/status/{code} accepte toutes les méthodes et répond avec le code du chemin. Il envoie aussi les en-têtes qu’un client attend avec ce statut : Retry-After sur 429 et 503, WWW-Authenticate sur 401, Location sur les redirections et Allow sur 405. Vous pouvez ainsi vérifier que votre code les lit, au lieu de regarder seulement le nombre.
$ 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/429Faire échouer un vrai point d’accès
Un test qui n’appelle que /api/status/500 ne prouve pas grand-chose sur l’écran qui charge les produits. Ajoutez error à la requête que fait déjà votre application, et cet appel échoue au lieu de renvoyer des données. ?delay=N attend d’abord N secondes, donc ?delay=3&error=503 permet de vérifier qu’un état de chargement s’affiche puis laisse place à un message d’erreur.
$ curl -i "https://api.testingapis.com/api/Products/1?error=503"
HTTP/1.1 503 Service Unavailable
x-simulated: true
{"status":503,"error":"Service Unavailable","message":"Simulated error. Use ?error={code} to test different status codes.","simulated":true}/api/Products/1?error=503Les deux paramètres marchent sur tous les points d’accès, écritures comprises. La liste complète est dans la documentation de l’API.
Tester les nouvelles tentatives sans deviner
Les échecs aléatoires rendent instables les tests de nouvelles tentatives. /api/chaos/retry/{key}connectez-vous pour voir est déterministe : avec succeedAfter=2, les deux premières requêtes pour une clé échouent et la troisième réussit. failStatus choisit le statut d’échec (429, 500, 502, 503 ou 504), et chaque réponse porte un en-tête X-Attempt, si bien qu’un test peut vérifier combien de tentatives votre client a faites. Utilisez une clé neuve à chaque exécution, ou réinitialisez-la avec DELETE.
$ curl -i "https://api.testingapis.com/api/chaos/retry/my-test?succeedAfter=2&failStatus=429"
HTTP/1.1 429 Too Many Requests
retry-after: 1
x-attempt: 1
$ curl -i "https://api.testingapis.com/api/chaos/retry/my-test?succeedAfter=2&failStatus=429"
HTTP/1.1 429 Too Many Requests
retry-after: 1
x-attempt: 2
$ curl -i "https://api.testingapis.com/api/chaos/retry/my-test?succeedAfter=2&failStatus=429"
HTTP/1.1 200 OK
x-attempt: 3
$ curl -X DELETE https://api.testingapis.com/api/chaos/retry/my-test
{"message":"Counter for 'my-test' reset after 3 attempt(s).","key":"my-test"}Voici un test pour un client qui réessaie, avec le test runner intégré de Node. Un fetchWithRetry complet, avec backoff exponentiel et prise en charge de Retry-After, se trouve dans la leçon sur les timeouts, nouvelles tentatives et limites de débit (en anglais).
import { test } from "node:test";
import assert from "node:assert/strict";
import { fetchWithRetry } from "./client.js"; // your code under test
test("retries a 503 and returns the eventual 200", async () => {
const key = `ci-${Date.now()}`;
const res = await fetchWithRetry(
`https://api.testingapis.com/api/chaos/retry/${key}?succeedAfter=2`,
);
assert.equal(res.status, 200);
assert.equal(res.headers.get("x-attempt"), "3");
});
test("gives up on a 400 without retrying", async () => {
const res = await fetchWithRetry("https://api.testingapis.com/api/status/400");
assert.equal(res.status, 400);
});Timeouts et connexions coupées
Certaines pannes ne produisent jamais de code de statut. Votre client doit les remarquer lui-même, et un script qui ne vérifie que res.ok ne les voit jamais. Ce script appelle cinq points d’accès en échec et affiche ce que fetch fait de chacun :
const BASE = "https://api.testingapis.com";
async function attempt(label, url, init) {
try {
const res = await fetch(url, init);
const body = await res.text();
console.log(label.padEnd(8), res.status, res.headers.get("retry-after") ?? "-", body.slice(0, 50));
} catch (err) {
console.log(label.padEnd(8), "threw", err.name, err.cause?.code ?? err.message);
}
}
await attempt("500", `${BASE}/api/status/500`);
await attempt("429", `${BASE}/api/status/429`);
await attempt("error=", `${BASE}/api/Products/1?error=503`);
await attempt("timeout", `${BASE}/api/chaos/timeout?seconds=10`, { signal: AbortSignal.timeout(2000) });
await attempt("partial", `${BASE}/api/chaos/partial`);500 500 - {"status":500,"statusText":"Internal Server Error
429 429 5 {"status":429,"statusText":"Too Many Requests","c
error= 503 - {"status":503,"error":"Service Unavailable","mes
timeout threw TimeoutError The operation was aborted due to timeout
partial threw TypeError UND_ERR_SOCKETLe timeout et le corps tronqué lèvent une exception au lieu de renvoyer une réponse. Le timeout la lève parce que AbortSignal.timeout(2000) a abandonné au bout de deux secondes. Sans signal, fetch aurait attendu les dix. La réponse partielle la lève à la lecture du corps, alors que le statut et les en-têtes étaient arrivés avec l’air normal. curl signale le même problème :
$ curl -sS -o /dev/null https://api.testingapis.com/api/chaos/partial
curl: (18) end of response with 9936 bytes missingUne vraie limite de débit
/api/status/429 simule une seule réponse. /api/RateLimitconnectez-vous pour voir est un vrai limiteur à fenêtre fixe : cinq requêtes par 60 secondes et par client, avec X-RateLimit-Remaining qui décompte et un 429 avec un Retry-After exact à la sixième. Envoyez un en-tête X-Client-Id pour que vos tests aient leur propre fenêtre, et POST /api/RateLimit/resetconnectez-vous pour voir la remet à zéro. La référence de la limite de débit détaille chaque en-tête.
/api/RateLimitCe qu’il faut vérifier dans votre client
- Les erreurs 4xx autres que 408 et 429 ne sont pas relancées : une mauvaise requête reste mauvaise.
- 429 et 503 attendent
Retry-Afterau lieu de réessayer aussitôt. - Les nouvelles tentatives s’arrêtent après un nombre maximal, et la dernière erreur parvient à l’utilisateur.
- Chaque requête a un timeout, et un timeout affiche une erreur au lieu d’un indicateur de chargement sans fin.
- Un corps illisible ou incomplet est une erreur, même avec un statut 200.
- Le
messaged’un corps d’erreur est affiché ou consigné, pas avalé.