目次

  1. 基本原則
  2. HTTPステータスコードの使い分け
  3. レスポンスボディの構造
  4. エラーコード設計
  5. メッセージの書き方
  6. セキュリティ上の注意
  7. リトライとレート制限
  8. 運用・可観測性
  9. ドキュメントとバージョニング
  10. 特殊なケース
  11. チェックリスト
  12. 参考

1. 基本原則

原則内容
一貫性すべてのエンドポイントで同じ構造のエラーを返す
機械可読性クライアントが分岐処理できる安定したエラーコードを含める
人間可読性開発者が原因を理解できるメッセージを含める
適切なHTTPステータスステータスコードだけで大まかな状況がわかるようにする
安全性内部情報(スタックトレース、SQL、内部パスなど)を漏らさない
対処可能性「何が悪くて、どうすれば直るか」がわかるようにする

2. HTTPステータスコードの使い分け

4xx:クライアント側の問題(リクエストを直せば成功しうる)

コード用途例
400 Bad Requestリクエストの形式が不正JSONが壊れている、必須パラメータ欠落
401 Unauthorized認証されていないトークンなし・期限切れ・無効
403 Forbidden認証済みだが権限がない他人のリソースへのアクセス
404 Not Foundリソースが存在しない存在しないID
405 Method Not AllowedHTTPメソッドが非対応GET専用にPOST
409 Conflictリソースの状態と競合重複登録、楽観ロック失敗
410 Gone恒久的に削除された廃止済みAPI・削除済みリソース
413 Content Too Largeボディが大きすぎるアップロードサイズ超過
415 Unsupported Media TypeContent-Typeが非対応XMLを送った
422 Unprocessable Content形式は正しいが意味的に不正バリデーションエラー
429 Too Many Requestsレート制限超過Retry-After ヘッダーを併用

5xx:サーバー側の問題(クライアントは基本的に直せない)

コード用途
500 Internal Server Error予期しないエラー
502 Bad Gateway上流サービスからの不正な応答
503 Service Unavailableメンテナンス・過負荷(Retry-After を併用)
504 Gateway Timeout上流サービスのタイムアウト

よくあるアンチパターン

  • エラーなのに 200 OK を返し、ボディで "success": false を表現する
  • すべてのエラーを 400 または 500 にまとめる
  • 401 と 403 を混同する
  • バリデーションエラーで 500 を返す(入力チェック漏れ)

400 と 422 の使い分け:構文レベルの不正(パースできない)は 400、パースはできたがビジネスルール・値の制約に違反する場合は 422、と分けるのが一般的。チーム内で方針を決めて統一することが最も重要。


3. レスポンスボディの構造

推奨:RFC 9457(Problem Details for HTTP APIs)

RFC 9457(旧 RFC 7807)はHTTP APIのエラー表現の標準仕様。独自形式を作るより、これに準拠すると相互運用性が高まる。

  • Content-Type: application/problem+json
{
  "type": "https://api.example.com/problems/insufficient-balance",
  "title": "残高が不足しています",
  "status": 422,
  "detail": "口座残高 3,000円 に対し、5,000円 の出金がリクエストされました。",
  "instance": "/accounts/12345/withdrawals/abc-789"
}
フィールド説明
typeエラー種別を識別するURI。ドキュメントへのリンクにすると親切
titleエラー種別の短い要約(同じ type なら同じ文言)
statusHTTPステータスコード
detailこの発生事例に固有の説明
instance問題が発生した具体的なリソース・リクエストの識別子

標準フィールドに加えて、独自の拡張フィールドを追加できる。

拡張例:実運用で便利なフィールド

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "入力内容に誤りがあります",
  "status": 422,
  "detail": "2件のフィールドでバリデーションエラーが発生しました。",
  "instance": "/users",
  "code": "VALIDATION_ERROR",
  "request_id": "req_7f3a9c2e1b",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "メールアドレスの形式が正しくありません。"
    },
    {
      "field": "age",
      "code": "OUT_OF_RANGE",
      "message": "年齢は0以上150以下で指定してください。",
      "meta": { "min": 0, "max": 150 }
    }
  ]
}
フィールド目的
codeクライアントが分岐に使う安定した文字列コード
request_id / trace_idログ調査・問い合わせ対応用
errors[]複数のフィールドエラーをまとめて返す
errors[].field問題のあるフィールド(ネストは address.zip や JSON Pointer /address/zip)
meta制約値など、クライアントがUI表示に使える補足情報

4. エラーコード設計

ポイント

  • 安定性:一度公開したコードは変えない(クライアントが依存する)
  • 文字列を使う:"RESOURCE_NOT_FOUND" のような意味のある文字列にする。10234 のような数値は可読性が低い
  • 粒度:クライアントが異なる処理をしたいケースごとに分ける
  • 階層化:必要に応じて AUTH_TOKEN_EXPIRED、AUTH_TOKEN_INVALID のようにプレフィックスで分類
  • 一覧をドキュメント化:全コードと意味、対処方法を公開する

例

codeHTTP意味クライアントの対処
AUTH_TOKEN_EXPIRED401トークン期限切れリフレッシュして再試行
AUTH_TOKEN_INVALID401トークン不正再ログイン
PERMISSION_DENIED403権限不足操作不可を表示
RESOURCE_NOT_FOUND404対象が存在しない一覧に戻す
DUPLICATE_RESOURCE409重複既存を案内
VERSION_CONFLICT409楽観ロック失敗再取得して再編集
VALIDATION_ERROR422入力値不正フィールドごとにエラー表示
RATE_LIMITED429レート制限Retry-After 秒後に再試行
INTERNAL_ERROR500内部エラー汎用エラー表示+request_id 提示

5. メッセージの書き方

良いメッセージの条件

  • 何が起きたかを具体的に書く
  • 可能なら、どうすれば解決するかを書く
  • 専門用語や内部用語を避ける
悪い例良い例
Error occurredメールアドレスの形式が正しくありません。
Invalid inputstart_date は end_date より前の日付を指定してください。
NullPointerException at UserService.java:42ユーザー情報の取得中にエラーが発生しました。(request_id: req_xxx)

開発者向けとエンドユーザー向けを区別する

  • detail / message は開発者向けと割り切り、エンドユーザー向け文言はクライアント側で code から生成する設計が柔軟
  • エンドユーザー向け文言をAPIから返す場合は、Accept-Language ヘッダーに応じた多言語化を検討する

6. セキュリティ上の注意

  • スタックトレース・SQL・内部ホスト名・ファイルパスを返さない(本番環境では必ず無効化)
  • 認証エラーの情報を絞る:ログイン失敗時に「ユーザーが存在しない」「パスワードが違う」を区別すると、アカウントの存在確認に悪用される
  • 存在の秘匿:権限のないリソースには 403 ではなく 404 を返し、存在自体を隠す選択肢もある
  • 入力値のエコーに注意:エラーメッセージに入力値をそのまま埋め込むとXSSやログインジェクションの原因になる。エスケープするか、含めない
  • 詳細な原因はサーバーログに記録し、レスポンスには request_id だけを返して紐付ける

7. リトライとレート制限

  • リトライ可能かを明示する:429、503、一部の 502 / 504 はリトライ対象、4xx(429除く)は基本的にリトライしても無駄
  • Retry-After ヘッダーを 429 / 503 で返す
  • レート制限情報のヘッダーを返すとクライアントが制御しやすい
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
  • 冪等性キー(Idempotency-Key ヘッダー)をサポートすると、POSTのリトライでも二重処理を防げる
  • クライアントには指数バックオフ+ジッターでのリトライを推奨する旨をドキュメントに記載

8. 運用・可観測性

  • すべてのレスポンス(エラー含む)に request_id を付与し、レスポンスヘッダー(例:X-Request-Id)にも含める
  • 分散トレーシングを使っている場合は traceparent(W3C Trace Context)と連携
  • エラーを code 単位で集計・監視し、5xxの急増や特定コードの増加をアラート対象にする
  • 4xxの多発はクライアント実装やドキュメントの問題のサインとして分析する

9. ドキュメントとバージョニング

  • OpenAPI等でエラーレスポンスのスキーマをエンドポイントごとに定義する(どのエラーが返りうるか明記)
  • type のURIで各エラーの解説ページを公開する
  • エラーコードの追加は後方互換、削除・意味の変更は破壊的変更として扱う
  • クライアントは未知のエラーコードを受け取っても壊れないよう、HTTPステータスによるフォールバック処理を持つべき、と明記する

10. 特殊なケース

バッチ・一括処理

一部成功・一部失敗の場合は、207 Multi-Status もしくは 200 で個別結果を返す。

{
  "results": [
    { "id": "1", "status": "success" },
    { "id": "2", "status": "error", "error": { "code": "VALIDATION_ERROR", "message": "..." } }
  ]
}

非同期処理

202 Accepted で受け付け、ジョブのステータス取得APIでエラー詳細を返す。ジョブ自体の失敗はジョブリソースの状態として表現し、ステータス取得APIは 200 で返す。

GraphQL

HTTPステータスは原則 200 で、errors 配列に extensions.code を含める慣習がある。REST向けの設計をそのまま当てはめない。


11. チェックリスト

  • 全エンドポイントでエラー形式が統一されている
  • 適切なHTTPステータスコードを使っている(エラーで200を返していない)
  • 機械可読な安定したエラーコードがある
  • 人間が理解できる具体的なメッセージがある
  • バリデーションエラーはフィールド単位で複数返せる
  • request_id が含まれ、サーバーログと紐付けられる
  • スタックトレース等の内部情報が本番で漏れない
  • 429 / 503 で Retry-After を返している
  • エラーコード一覧と対処方法がドキュメント化されている
  • OpenAPI等でエラースキーマが定義されている

参考

0件のページ