Una API simulada que devuelve errores cuando quieras
Consigue un 500, un 429 con Retry-After, un timeout o un cuerpo truncado siempre que tus tests lo necesiten.
Ejemplos ejecutados por última vez contra la API el
Para probar cómo maneja tu código una API que falla, apúntalo a un endpoint que falle a propósito. https://api.testingapis.com es gratuita, no necesita clave y acepta peticiones desde cualquier origen. /api/status/500 devuelve un 500 y /api/status/429 un 429 con Retry-After. Funciona cualquier código del 200 al 599. Añadir ?error=503 a un endpoint real lo hace fallar, y /api/chaos/retry/{key}inicia sesión para verlo falla un número fijo de veces antes de acertar. /api/chaos/timeoutinicia sesión para verlo se queda colgado hasta que tu cliente se rinde, y /api/chaos/partialinicia sesión para verlo envía media respuesta. Aquí tienes cada uno, con la respuesta que devuelve.
Qué endpoint para cada fallo
| Quieres probar | Petición | Lo que vuelve |
|---|---|---|
| Un código de estado concreto | GET /api/status/503 | Ese estado, un cuerpo JSON y las cabeceras que suele llevar |
| Un endpoint real que falla | GET /api/Products/1?error=500 | El estado que pediste, con X-Simulated: true |
| Un endpoint real lento | GET /api/Products/1?delay=3 | La respuesta normal tras N segundos (máximo 10) |
| Lógica de reintentos | GET /api/chaos/retry/my-key?succeedAfter=2inicia sesión para verlo | Dos fallos y luego 200. X-Attempt cuenta los intentos |
| Fallos aleatorios | GET /api/chaos/flaky?failRate=0.3inicia sesión para verlo | 503 con Retry-After: 1 aproximadamente el 30 % de las veces |
| Un timeout del cliente | GET /api/chaos/timeout?seconds=30inicia sesión para verlo | Nada hasta que pasa el tiempo (de 1 a 60 segundos) |
| Un cuerpo cortado | GET /api/chaos/partialinicia sesión para verlo | Las cabeceras prometen 10.000 bytes, llegan unos 64 y la conexión se cierra |
| Un límite de frecuencia real | GET /api/RateLimitinicia sesión para verlo | 429 tras 5 peticiones en 60 segundos, con Retry-After |
| Una mezcla ponderada | GET /api/status/random?codes=200:3,500:1 | 200 tres veces más a menudo que 500 |
Devolver cualquier código de estado HTTP
/api/status/{code} acepta cualquier método y responde con el código de la ruta. También envía las cabeceras que un cliente esperaría con ese estado: Retry-After en 429 y 503, WWW-Authenticate en 401, Location en las redirecciones y Allow en 405. Así puedes comprobar que tu código las lee y no se fija solo en el número.
$ 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/429Hacer fallar un endpoint real
Un test que solo llama a /api/status/500 no demuestra mucho sobre la pantalla que carga productos. Añade error a la petición que ya hace tu app, y esa llamada falla en lugar de devolver datos. ?delay=N espera antes N segundos, así que ?delay=3&error=503 te permite comprobar que aparece un estado de carga y que luego da paso a un mensaje de error.
$ 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=503Los dos parámetros funcionan en todos los endpoints, también en las escrituras. La lista completa está en la documentación de la API.
Probar reintentos sin adivinar
Los fallos aleatorios vuelven inestables los tests de reintentos. /api/chaos/retry/{key}inicia sesión para verlo es determinista: con succeedAfter=2, las dos primeras peticiones para una clave fallan y la tercera acierta. failStatus elige el estado del fallo (429, 500, 502, 503 o 504), y cada respuesta lleva una cabecera X-Attempt, así que un test puede comprobar cuántos intentos hizo tu cliente. Usa una clave nueva en cada ejecución, o reiníciala con 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"}Este es un test para un cliente que reintenta, con el test runner integrado de Node. Un fetchWithRetry completo, con backoff exponencial y soporte de Retry-After, está en la lección de timeouts, reintentos y límites de frecuencia (en inglés).
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 y conexiones rotas
Algunos fallos nunca producen un código de estado. Tu cliente tiene que detectarlos por sí mismo, y un script que solo mira res.ok nunca los ve. Este script llama a cinco endpoints que fallan e imprime qué hace fetch con cada uno:
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_SOCKETEl timeout y el cuerpo cortado lanzan una excepción en lugar de devolver una respuesta. El timeout la lanza porque AbortSignal.timeout(2000) se rindió a los dos segundos. Sin señal, fetch habría esperado los diez. La respuesta parcial la lanza al leer el cuerpo, cuando el estado y las cabeceras ya habían llegado con buena pinta. curl informa del mismo problema:
$ curl -sS -o /dev/null https://api.testingapis.com/api/chaos/partial
curl: (18) end of response with 9936 bytes missingUn limitador de frecuencia real
/api/status/429 simula una sola respuesta. /api/RateLimitinicia sesión para verlo es un limitador real de ventana fija: cinco peticiones cada 60 segundos por cliente, con X-RateLimit-Remaining descontando y un 429 con un Retry-After exacto en la sexta. Envía una cabecera X-Client-Id para que tus tests tengan su propia ventana, y POST /api/RateLimit/resetinicia sesión para verlo la vacía. La referencia del límite de frecuencia tiene todas las cabeceras.
/api/RateLimitQué comprobar en tu cliente
- Los errores 4xx que no son 408 ni 429 no se reintentan: una petición mala sigue siendo mala.
- 429 y 503 esperan a
Retry-Afteren lugar de reintentar al momento. - Los reintentos paran tras un máximo de intentos, y el último error llega al usuario.
- Cada petición tiene un timeout, y un timeout muestra un error en lugar de un spinner eterno.
- Un cuerpo que no se puede analizar o llega incompleto es un error, aunque el estado sea 200.
- El
messagede un cuerpo de error se muestra o se registra, no se pierde.