好きなときにエラーを返すモック 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サインインして表示 | 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 の 3 倍の頻度で返る |
任意の 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 なら、あるキーへの最初の 2 回は失敗し、3 回目で成功します。failStatus で失敗時のステータス(429、500、502、503、504)を選べ、すべてのレスポンスに X-Attempt ヘッダーが付くので、クライアントが何回試行したかをテストで確認できます。テストの実行ごとに新しいキーを使うか、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 しか見ないスクリプトには見えません。このスクリプトは失敗する 5 つのエンドポイントを呼び、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) が 2 秒であきらめたためです。signal がなければ、fetch は 10 秒まるごと待って いたでしょう。部分的なレスポンスは、ステータスとヘッダーが正常に見える形で届いた後、ボディを読む途中で例外を投げます。 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 は 1 回のレスポンスを装うだけです。/api/RateLimitサインインして表示 は本物の固定ウィンドウ方式のリミッターで、クライアントごとに 60 秒あたり 5 リクエストまで。X-RateLimit-Remaining が減っていき、6 回目で正確な Retry-After 付きの 429 が返ります。X-Client-Id ヘッダーを送ればテストごとに専用のウィンドウになり、POST /api/RateLimit/resetサインインして表示 でリセットできます。すべてのヘッダーはレート制限のリファレンスにあります。
/api/RateLimitクライアントで確認すること
- 408 と 429 以外の 4xx エラーはリトライしない。悪いリクエストは何度送っても悪いまま。
- 429 と 503 はすぐにリトライせず、
Retry-Afterを待つ。 - リトライは最大回数で止まり、最後のエラーがユーザーに届く。
- すべてのリクエストにタイムアウトがあり、タイムアウトしたら終わらないスピナーではなくエラーを表示する。
- 解析できない、または不完全なボディは、ステータスが 200 でもエラーとして扱う。
- エラーボディの
messageは握りつぶさず、表示するかログに残す。