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 testerRequêteCe qui revient
Un code de statut précisGET /api/status/503Ce statut, un corps JSON et les en-têtes qu’il porte d’habitude
Un vrai point d’accès en échecGET /api/Products/1?error=500Le statut demandé, avec X-Simulated: true
Un vrai point d’accès lentGET /api/Products/1?delay=3La réponse normale après N secondes (10 au maximum)
La logique de nouvelle tentativeGET /api/chaos/retry/my-key?succeedAfter=2connectez-vous pour voirDeux échecs, puis 200. X-Attempt compte les tentatives
Des échecs aléatoiresGET /api/chaos/flaky?failRate=0.3connectez-vous pour voir503 avec Retry-After: 1 environ 30 % du temps
Un timeout du clientGET /api/chaos/timeout?seconds=30connectez-vous pour voirRien jusqu’à la fin du délai (de 1 à 60 secondes)
Un corps tronquéGET /api/chaos/partialconnectez-vous pour voirLes en-têtes annoncent 10 000 octets, environ 64 arrivent, puis la connexion se ferme
Une vraie limite de débitGET /api/RateLimitconnectez-vous pour voir429 après 5 requêtes en 60 secondes, avec Retry-After
Un mélange pondéréGET /api/status/random?codes=200:3,500:1200 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.

Terminal
$ 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."}
GET/api/status/429

Faire é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.

Terminal
$ 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}
GET/api/Products/1?error=503

Les 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.

Terminal
$ 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"}
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit

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).

JavaScript
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);
});
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit

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 :

failures.mjs
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`);
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit
Sortie (Node 24)
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_SOCKET

Le 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 :

Terminal
$ curl -sS -o /dev/null https://api.testingapis.com/api/chaos/partial
curl: (18) end of response with 9936 bytes missing
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit

Une 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.

GET/api/RateLimit
Cette requête utilise un point d’accès réservé aux membres connectés.Se connecterCréer un compte gratuit

Ce 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-After au 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 message d’un corps d’erreur est affiché ou consigné, pas avalé.