TestingAPIs is still being built. If something breaks, email support@testingapis.com.

A mock API that returns errors on demand

Get a 500, a 429 with Retry-After, a timeout or a truncated body whenever your tests need one.

Examples last run against the API on

To test how your code handles a failing API, point it at an endpoint that fails on purpose. https://api.testingapis.com is free and allows requests from any origin. Status codes need no key; the failure injection endpoints need a free account's API key in the X-TestingApis-Key header. /api/status/500 returns a 500 and /api/status/429 a 429 with Retry-After. Any code from 200 to 599 works. Adding ?error=503 to a real endpoint makes it fail, and /api/chaos/retry/{key}sign in to see fails a set number of times before it succeeds. /api/chaos/timeoutsign in to see hangs until your client gives up, and /api/chaos/partialsign in to seesends half a response. Here's each one, with the response it returns.

Which endpoint for which failure

You want to testRequestWhat comes back
A specific status codeGET /api/status/503That status, a JSON body, and the headers it normally carries
A failing real endpointGET /api/Products/1?error=500The status you asked for, with X-Simulated: true
A slow real endpointGET /api/Products/1?delay=3The normal response after N seconds (maximum 10)
Retry logicGET /api/chaos/retry/my-key?succeedAfter=2sign in to seeTwo failures, then 200. X-Attempt counts the attempts
Random failuresGET /api/chaos/flaky?failRate=0.3sign in to see503 with Retry-After: 1 about 30% of the time
A client timeoutGET /api/chaos/timeout?seconds=30sign in to seeNothing until the time is up (1 to 60 seconds)
A cut-off bodyGET /api/chaos/partialsign in to seeHeaders promise 10,000 bytes, about 64 arrive, then the connection closes
Real rate limitingGET /api/RateLimitsign in to see429 after 5 requests in 60 seconds, with Retry-After
A weighted mixGET /api/status/random?codes=200:3,500:1200 three times as often as 500

Return any HTTP status code

/api/status/{code} accepts any method and responds with the code in the path. It also sends the headers a client would expect with that status: Retry-After on 429 and 503, WWW-Authenticate on 401, Location on redirects and Allowon 405. So you can check that your code reads them and doesn't just look at the number.

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

Make a real endpoint fail

A test that only ever calls /api/status/500doesn't prove much about the screen that loads products. Add error to the request your app already makes, and that call fails instead of returning data. ?delay=N waits N seconds first, so ?delay=3&error=503 lets you check that a loading state appears and then gives way to an error message.

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

Both parameters work on every endpoint, including writes. The full list is in the API documentation.

Test retries without guessing

Random failures make retry tests flaky. /api/chaos/retry/{key}sign in to see is deterministic: with succeedAfter=2, the first two requests for a key fail and the third succeeds. failStatus picks the failure status (429, 500, 502, 503 or 504), and every response has an X-Attempt header, so a test can assert how many attempts your client made. Use a fresh key per test run, or reset it with 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"}
This example uses endpoints for signed-in members.Sign inCreate a free account

Here's a test for a retrying client, using Node's built-in test runner. A complete fetchWithRetry with exponential backoff and Retry-After support is in the timeouts, retries and rate limits lesson.

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);
});
This example uses endpoints for signed-in members.Sign inCreate a free account

Timeouts and broken connections

Some failures never produce a status code. Your client has to notice them itself, and a script that only checks res.ok never sees them. This script calls five failing endpoints and prints what fetch does with each:

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`);
This example uses endpoints for signed-in members.Sign inCreate a free account
Output (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

The timeout and the cut-off body both throw instead of returning a response. The timeout throws because AbortSignal.timeout(2000) gave up after two seconds. Without a signal, fetch would have waited the full ten. The partial response throws while reading the body, after the status and headers arrived looking healthy. curl reports the same problem:

Terminal
$ curl -sS -o /dev/null https://api.testingapis.com/api/chaos/partial
curl: (18) end of response with 9936 bytes missing
This example uses endpoints for signed-in members.Sign inCreate a free account

A real rate limiter

/api/status/429 fakes a single response. /api/RateLimitsign in to see is a real fixed-window limiter: five requests per 60 seconds per client, with X-RateLimit-Remaining counting down and a 429 with an accurate Retry-After on the sixth. Send an X-Client-Id header so your tests get their own window, and POST /api/RateLimit/resetsign in to see clears it. The rate limit reference has every header.

GET/api/RateLimit
This request uses an endpoint for signed-in members.Sign inCreate a free account

What to check in your client

  • 4xx errors other than 408 and 429 are not retried: a bad request stays bad.
  • 429 and 503 wait for Retry-After instead of retrying at once.
  • Retries stop after a maximum number of attempts, and the last error reaches the user.
  • Every request has a timeout, and a timeout shows an error instead of an endless spinner.
  • A body that fails to parse or arrives incomplete is an error, even with a 200 status.
  • An error body's message is shown or logged, not swallowed.