一个按需返回错误的模拟 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=3 | N 秒后返回正常响应(最多 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:1 | 200 出现的次数是 500 的三倍 |
返回任意 HTTP 状态码
/api/status/{code} 接受任何方法,并以路径中的状态码响应。它还会发送客户端期望该状态带有的响应头:429 和 503 带 Retry-After,401 带 WWW-Authenticate,重定向带 Location,405 带 Allow。 这样你就能检查代码是否读取了这些响应头,而不只是看数字。
$ 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/429让真实端点出错
只调用 /api/status/500 的测试,对加载商品的页面证明不了什么。在你的应用已经发出的请求上加上 error, 这个调用就会失败而不是返回数据。?delay=N 会先等待 N 秒,所以用 ?delay=3&error=503 可以检查先出现加载状态、然后变成错误提示。
$ 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=503这两个参数适用于所有端点,包括写操作。完整列表见 API 文档。
不靠运气测试重试
随机失败会让重试测试变得不稳定。/api/chaos/retry/{key}登录后查看 是确定性的:设置 succeedAfter=2 时,对同一个 key 的前两次请求失败,第三次成功。failStatus 选择失败时的状态码(429、500、502、503 或 504), 每个响应都带 X-Attempt 响应头,所以测试可以断言客户端尝试了几次。每次测试运行使用新的 key,或者用 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"}下面是一个针对会重试的客户端的测试,使用 Node 内置的测试运行器。带指数退避和 Retry-After 支持的完整 fetchWithRetry 见超时、重试和限流课程(英文)。
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 对每个端点的处理结果:
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_SOCKET超时和被截断的响应体都会抛出异常,而不是返回响应。超时抛出异常,是因为 AbortSignal.timeout(2000) 在两秒后放弃了。如果没有 signal,fetch 会一直等满十秒。不完整的响应则是在读取响应体时抛出异常,而此前状态码和响应头看起来都很正常。 curl 报告的是同一个问题:
$ 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登录后查看 会清空它。所有响应头见限流参考。
/api/RateLimit在客户端中要检查什么
- 除 408 和 429 以外的 4xx 错误不重试:错误的请求重发还是错误的。
- 429 和 503 要等待
Retry-After,而不是立即重试。 - 重试达到最大次数后停止,最后一个错误要传达给用户。
- 每个请求都有超时,超时后显示错误,而不是无休止地转圈。
- 无法解析或不完整的响应体就是错误,即使状态码是 200。
- 错误响应体中的
message要显示或记录下来,不能吞掉。