目次
- 基本原則
- HTTPステータスコードの使い分け
- レスポンスボディの構造
- エラーコード設計
- メッセージの書き方
- セキュリティ上の注意
- リトライとレート制限
- 運用・可観測性
- ドキュメントとバージョニング
- 特殊なケース
- チェックリスト
- 参考
1. 基本原則
| 原則 | 内容 |
|---|
| 一貫性 | すべてのエンドポイントで同じ構造のエラーを返す |
| 機械可読性 | クライアントが分岐処理できる安定したエラーコードを含める |
| 人間可読性 | 開発者が原因を理解できるメッセージを含める |
| 適切なHTTPステータス | ステータスコードだけで大まかな状況がわかるようにする |
| 安全性 | 内部情報(スタックトレース、SQL、内部パスなど)を漏らさない |
| 対処可能性 | 「何が悪くて、どうすれば直るか」がわかるようにする |
2. HTTPステータスコードの使い分け
4xx:クライアント側の問題(リクエストを直せば成功しうる)
| コード | 用途 | 例 |
|---|
| 400 Bad Request | リクエストの形式が不正 | JSONが壊れている、必須パラメータ欠落 |
| 401 Unauthorized | 認証されていない | トークンなし・期限切れ・無効 |
| 403 Forbidden | 認証済みだが権限がない | 他人のリソースへのアクセス |
| 404 Not Found | リソースが存在しない | 存在しないID |
| 405 Method Not Allowed | HTTPメソッドが非対応 | GET専用にPOST |
| 409 Conflict | リソースの状態と競合 | 重複登録、楽観ロック失敗 |
| 410 Gone | 恒久的に削除された | 廃止済みAPI・削除済みリソース |
| 413 Content Too Large | ボディが大きすぎる | アップロードサイズ超過 |
| 415 Unsupported Media Type | Content-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 なら同じ文言) |
status | HTTPステータスコード |
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 のようにプレフィックスで分類
- 一覧をドキュメント化:全コードと意味、対処方法を公開する
例
| code | HTTP | 意味 | クライアントの対処 |
|---|
AUTH_TOKEN_EXPIRED | 401 | トークン期限切れ | リフレッシュして再試行 |
AUTH_TOKEN_INVALID | 401 | トークン不正 | 再ログイン |
PERMISSION_DENIED | 403 | 権限不足 | 操作不可を表示 |
RESOURCE_NOT_FOUND | 404 | 対象が存在しない | 一覧に戻す |
DUPLICATE_RESOURCE | 409 | 重複 | 既存を案内 |
VERSION_CONFLICT | 409 | 楽観ロック失敗 | 再取得して再編集 |
VALIDATION_ERROR | 422 | 入力値不正 | フィールドごとにエラー表示 |
RATE_LIMITED | 429 | レート制限 | Retry-After 秒後に再試行 |
INTERNAL_ERROR | 500 | 内部エラー | 汎用エラー表示+request_id 提示 |
5. メッセージの書き方
良いメッセージの条件
- 何が起きたかを具体的に書く
- 可能なら、どうすれば解決するかを書く
- 専門用語や内部用語を避ける
| 悪い例 | 良い例 |
|---|
Error occurred | メールアドレスの形式が正しくありません。 |
Invalid input | start_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. チェックリスト
参考