一个按需返回错误的模拟 API

随时为你的测试提供 500、带 Retry-After 的 429、超时或被截断的响应体。

示例最后一次针对 API 运行于

要测试你的代码如何应对出错的 API,就把它指向一个故意出错的端点。https://api.testingapis.com 免费、无需密钥,并允许任何来源的请求。/api/status/500 返回 500,/api/status/429 返回带 Retry-After 的 429,200 到 599 之间的任何状态码都可以。给真实端点加上 ?error=503 就会让它失败,/api/chaos/retry/{key}登录后查看 会先失败固定的次数再成功。/api/chaos/timeout登录后查看 会一直挂起直到你的客户端放弃,/api/chaos/partial登录后查看 只发送一半的响应。下面逐个介绍,并附上它们返回的响应。

哪种故障用哪个端点

你想测试请求返回什么
某个特定状态码GET /api/status/503该状态码、一个 JSON 响应体,以及它通常带的响应头
一个出错的真实端点GET /api/Products/1?error=500你指定的状态码,带 X-Simulated: true
一个很慢的真实端点GET /api/Products/1?delay=3N 秒后返回正常响应(最多 10 秒)
重试逻辑GET /api/chaos/retry/my-key?succeedAfter=2登录后查看两次失败后返回 200。X-Attempt 记录尝试次数
随机失败GET /api/chaos/flaky?failRate=0.3登录后查看约 30% 的情况下返回带 Retry-After: 1 的 503
客户端超时GET /api/chaos/timeout?seconds=30登录后查看时间到之前什么都不返回(1 到 60 秒)
被截断的响应体GET /api/chaos/partial登录后查看响应头声明 10,000 字节,只到达约 64 字节,然后连接关闭
真实的限流GET /api/RateLimit登录后查看60 秒内超过 5 个请求后返回带 Retry-After 的 429
按权重混合GET /api/status/random?codes=200:3,500:1200 出现的次数是 500 的三倍

返回任意 HTTP 状态码

/api/status/{code} 接受任何方法,并以路径中的状态码响应。它还会发送客户端期望该状态带有的响应头:429 和 503 带 Retry-After,401 带 WWW-Authenticate,重定向带 Location,405 带 Allow。 这样你就能检查代码是否读取了这些响应头,而不只是看数字。

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

让真实端点出错

只调用 /api/status/500 的测试,对加载商品的页面证明不了什么。在你的应用已经发出的请求上加上 error, 这个调用就会失败而不是返回数据。?delay=N 会先等待 N 秒,所以用 ?delay=3&error=503 可以检查先出现加载状态、然后变成错误提示。

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

这两个参数适用于所有端点,包括写操作。完整列表见 API 文档。

不靠运气测试重试

随机失败会让重试测试变得不稳定。/api/chaos/retry/{key}登录后查看 是确定性的:设置 succeedAfter=2 时,对同一个 key 的前两次请求失败,第三次成功。failStatus 选择失败时的状态码(429、500、502、503 或 504), 每个响应都带 X-Attempt 响应头,所以测试可以断言客户端尝试了几次。每次测试运行使用新的 key,或者用 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"}
这个示例使用的是面向已登录成员的端点。登录创建免费账户

下面是一个针对会重试的客户端的测试,使用 Node 内置的测试运行器。带指数退避和 Retry-After 支持的完整 fetchWithRetry 见超时、重试和限流课程(英文)。

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);
});
这个示例使用的是面向已登录成员的端点。登录创建免费账户

超时和断开的连接

有些故障根本不会产生状态码。客户端必须自己发现它们,只检查 res.ok 的脚本永远看不到。这个脚本调用五个会出错的端点, 并打印 fetch 对每个端点的处理结果:

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`);
这个示例使用的是面向已登录成员的端点。登录创建免费账户
输出(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

超时和被截断的响应体都会抛出异常,而不是返回响应。超时抛出异常,是因为 AbortSignal.timeout(2000) 在两秒后放弃了。如果没有 signal,fetch 会一直等满十秒。不完整的响应则是在读取响应体时抛出异常,而此前状态码和响应头看起来都很正常。 curl 报告的是同一个问题:

Terminal
$ curl -sS -o /dev/null https://api.testingapis.com/api/chaos/partial
curl: (18) end of response with 9936 bytes missing
这个示例使用的是面向已登录成员的端点。登录创建免费账户

真实的限流器

/api/status/429 只是模拟一次响应。/api/RateLimit登录后查看 是一个真实的固定窗口限流器:每个客户端每 60 秒 5 个请求,X-RateLimit-Remaining 会递减,第六个请求返回带准确 Retry-After 的 429。发送 X-Client-Id 请求头可以让你的测试拥有自己的窗口,POST /api/RateLimit/reset登录后查看 会清空它。所有响应头见限流参考。

GET/api/RateLimit
这个请求使用的是面向已登录成员的端点。登录创建免费账户

在客户端中要检查什么

  • 除 408 和 429 以外的 4xx 错误不重试:错误的请求重发还是错误的。
  • 429 和 503 要等待 Retry-After,而不是立即重试。
  • 重试达到最大次数后停止,最后一个错误要传达给用户。
  • 每个请求都有超时,超时后显示错误,而不是无休止地转圈。
  • 无法解析或不完整的响应体就是错误,即使状态码是 200。
  • 错误响应体中的 message 要显示或记录下来,不能吞掉。