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 probarPeticiónLo que vuelve
Un código de estado concretoGET /api/status/503Ese estado, un cuerpo JSON y las cabeceras que suele llevar
Un endpoint real que fallaGET /api/Products/1?error=500El estado que pediste, con X-Simulated: true
Un endpoint real lentoGET /api/Products/1?delay=3La respuesta normal tras N segundos (máximo 10)
Lógica de reintentosGET /api/chaos/retry/my-key?succeedAfter=2inicia sesión para verloDos fallos y luego 200. X-Attempt cuenta los intentos
Fallos aleatoriosGET /api/chaos/flaky?failRate=0.3inicia sesión para verlo503 con Retry-After: 1 aproximadamente el 30 % de las veces
Un timeout del clienteGET /api/chaos/timeout?seconds=30inicia sesión para verloNada hasta que pasa el tiempo (de 1 a 60 segundos)
Un cuerpo cortadoGET /api/chaos/partialinicia sesión para verloLas cabeceras prometen 10.000 bytes, llegan unos 64 y la conexión se cierra
Un límite de frecuencia realGET /api/RateLimitinicia sesión para verlo429 tras 5 peticiones en 60 segundos, con Retry-After
Una mezcla ponderadaGET /api/status/random?codes=200:3,500:1200 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.

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

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

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

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

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"}
Este ejemplo usa endpoints para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta gratuita

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

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);
});
Este ejemplo usa endpoints para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta gratuita

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:

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`);
Este ejemplo usa endpoints para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta gratuita
Salida (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

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

Terminal
$ curl -sS -o /dev/null https://api.testingapis.com/api/chaos/partial
curl: (18) end of response with 9936 bytes missing
Este ejemplo usa endpoints para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta gratuita

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

GET/api/RateLimit
Esta petición usa un endpoint para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta gratuita

Qué 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-After en 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 message de un cuerpo de error se muestra o se registra, no se pierde.