応用読了目安 13 分#エラー設計#HTTPステータス#リトライ#API
エラー設計 — 呼び出し側が正しく振る舞える情報を返す
エラーは失敗の通知ではなく、次に何をすべきかの指示。リトライしてよいか、直せば通るかを伝える。
先に読んでおくとよい記事
この記事の進み方
What — ステータスコードが伝えていること
HTTP ステータスコードの百の位は、誰が何をすべきかを表しています。
2xx 成功正常に処理された200 / 201 / 204
3xx リダイレクト別の場所を見てほしい301 / 304
4xx クライアントエラーリクエストに問題がある。直さないと通らない400 / 401 / 403 / 404 / 409 / 422 / 429
5xx サーバーエラーサーバー側の問題。リクエストは正しいかもしれない500 / 502 / 503 / 504
1/4
2xx 成功 — 201 は作成、204 は成功したが返す本体がない。DELETE の成功は 204 が自然。
4xx は「あなたが直せば通る」、5xx は「こちらの問題なので、待てば通るかもしれない」。この区別がリトライの可否を決める。
よく使うコードの使い分け
- 400 Bad Request
- リクエストの形式が不正。JSON がパースできない、必須項目がない。
- 401 Unauthorized
- 認証されていない。ログインすれば通る可能性がある。名前に反して「認証」の話。
- 403 Forbidden
- 認証済みだが権限がない。ログインし直しても通らない。
- 404 Not Found
- リソースが存在しない。存在を隠したい場合、403 の代わりに使うこともある。
- 409 Conflict
- 現在の状態と矛盾する。すでにキャンセル済みの注文をキャンセルしようとした、など。
- 422 Unprocessable Entity
- 形式は正しいが内容が業務ルールに反する。メールアドレスの形式は正しいが既に使われている、など。
- 429 Too Many Requests
- レート制限。4xx だが、待てば通る。Retry-After ヘッダで待ち時間を伝える。
- 503 Service Unavailable
- 一時的に利用できない。メンテナンス中や過負荷。Retry-After を付けられる。
エラーレスポンスの本体に何を入れるか
何が悪いか分からず、次に何をすべきかも分からない。
HTTP 200 OK
{
"success": false,
"message": "エラーが発生しました"
}
クライアントにできること:
- メッセージをそのまま画面に出す
- 文字列で分岐する(サーバーの文言変更で壊れる)
- リトライすべきか判断できない- 機械可読性
- なし
- リトライ判定
- 不可能
- 監視での検知
- できない(全部 200)
- どこを直すか
- 分からない
- –文言で分岐すると、多言語化やメッセージ改善のたびにクライアントが壊れる。
- –ステータスが 200 だと、ロードバランサやプロキシの統計にもエラーとして現れない。
人間向けのメッセージだけでは、クライアントは分岐できない。機械が判定できるコードと、どのフィールドの問題かが必要。
確認 — ここまで読めたか
失敗も HTTP 200 で返し、ボディの success: false で表す方式。何が困りますか?
Why — エラーは「次に何をすべきか」の指示
呼び出し側がエラーを受け取ったとき、判断すべきことは3つです。
- リトライしてよいか — 一時的な問題か、直さないと通らないか
- 何を直せばよいか — どのフィールドの、どういう問題か
- 利用者に何を伝えるか — そのまま表示してよい文言か
この3つに答えられないエラーレスポンスは、呼び出し側に「とりあえず何か失敗した」としか伝えていません。
なぜ 200 で返してはいけないのか
「HTTP は転送の成功、業務エラーはボディで」という考え方には一理あります。しかし実務では次のコストが発生します。
- 監視が機能しない。 エラー率、SLO、アラートがすべて HTTP ステータスを基準にしている
- ロードバランサやプロキシが異常を検知できない。 5xx が続けばインスタンスを切り離す、といった仕組みが働かない
- クライアントのライブラリが素直に使えない。 多くの HTTP クライアントは、ステータスで例外を投げるか否かを決める
- CDN やキャッシュが誤ってキャッシュする。 200 はキャッシュ可能と解釈されうる
HTTP のエコシステム全体がステータスコードを前提に動いているので、そこから外れると多くの仕組みが無効になります。
内部情報を漏らさない
エラーメッセージは攻撃の手がかりになります。
// 危険
{ "message": "SQLSTATE[42S02]: Base table 'prod_db.users' doesn't exist" }
{ "message": "ユーザー ID 12345 は存在しますが、パスワードが違います" }
// 安全
{ "code": "INTERNAL_ERROR", "message": "処理に失敗しました", "traceId": "01H8X..." }
{ "code": "INVALID_CREDENTIALS", "message": "メールアドレスまたはパスワードが正しくありません" }
- スタックトレースや SQL を返さない。 テーブル構造やフレームワークのバージョンが漏れる
- 認証では「どちらが間違っているか」を言わない。 メールアドレスの存在確認に使われる
- 代わりに traceId を返す。 詳細はサーバーのログにあり、問い合わせ時に突き合わせられる
リトライの作法
5xx を受け取ったクライアントは、リトライしてよい。ただしやり方が重要です。
// 悪い:全員が同じタイミングで殺到する
for (let i = 0; i < 3; i++) {
try { return await call(); } catch { await sleep(1000); }
}
// 良い:指数バックオフ + ジッター
for (let i = 0; i < 3; i++) {
try { return await call(); }
catch (e) {
if (!isRetryable(e)) throw e; // 4xx はリトライしない
const base = 2 ** i * 200; // 200, 400, 800ms
await sleep(base + Math.random() * base); // ジッターで分散
}
}
ジッター(ランダムなずれ)が重要です。 固定間隔だと、障害から復旧した瞬間に全クライアントが同時にリトライし、再び落ちます(サンダリングハード)。
確認 — ここまで読めたか
エラー応答に含めるべき、呼び出し側がいちばん必要とする情報はどれですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
演習 — 設計判断を問う
この5つの失敗に、それぞれどう応答しますか?
与えられた条件
- オンライン英会話の予約 API: POST /reservations
- ① リクエストの JSON が壊れていてパースできない
- ② 認証トークンの有効期限が切れている
- ③ 予約しようとした枠が、直前に他の人に取られていた
- ④ 保有チケットが足りない(予約には2枚必要だが1枚しかない)
- ⑤ 予約 DB への接続がタイムアウトした
- クライアントは Web とモバイルアプリの2種類。モバイルは自動リトライを実装している
この軸で考える
- · そのリクエストを再送したら成功する可能性があるか
- · 利用者が何かすれば通るのか、待てば通るのか、どちらでもないのか
- · モバイルの自動リトライが、どのケースで動くべきか
演習 — 説明できるか
「エラーは全部 200 とメッセージで返す方針にしたい」という提案に、何が起きるかを説明してください
与えられた条件
- 相手はクライアント側の分岐を減らしたい
- 監視とリトライの仕組みが既にある
- 代案まで示したい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。