DEV Community

Cover image for REST APIエラー処理ベストプラクティス:ステータスコード、RFC 9457、リトライ可能なエラー
Akira
Akira

Posted on Originally published at apidog.com

REST APIエラー処理ベストプラクティス:ステータスコード、RFC 9457、リトライ可能なエラー

REST APIのエラー処理を設計する:ステータスコード、Problem Details、再試行、テスト

APIのエラー応答は契約の一部です。クライアントは応答を解析し、リトライロジックはそれに応じて分岐し、サポートエンジニアは午前2時にエラーをgrepで検索します。それにもかかわらず、正常系だけを詳細に設計し、エラー処理をフレームワークのデフォルトに任せるチームは少なくありません。その結果、1つのAPIに複数のエラー形式が存在したり、"success": falseを含む200応答が返ったり、データベーススキーマを漏らすスタックトレースが公開されたりします。

今すぐApidogを試す

このガイドでは、RESTサービスのエラー処理を契約として設計する方法を解説します。適切なHTTPステータスコード、RFC 9457 Problem Detailsによるエラー形式の統一、機械可読コードと人間向けメッセージの分離、再試行可能性の明示、機密情報の除外、そしてApidogを使った失敗パスのテストが対象です。

1. エラーボディより先にステータスコードを決める

HTTPは、エラーセマンティクスの第一層を提供します。RFC 9110では、4xxはクライアント側の問題、5xxはサーバー側の問題を表します。汎用クライアント、プロキシ、キャッシュ、リトライライブラリは、JSONを読む前にこの区分で動作を決めるため、エラーボディを設計する前にステータスコードを確定してください。

設計時はMDNのHTTPステータスコードリファレンスも参照し、迷いやすいケースには次の表を使います。

状況 使用 非推奨 理由
壊れたJSON、誤ったContent-Type、必須フィールドの欠落 400 Bad Request 422 サーバーがリクエストを解析・理解できない
整形式だが意味論的に不正(金額が負、未対応通貨など) 422 Unprocessable Content 400 構文は正しいが値が不正
認証情報がない、期限切れまたは無効なトークン 401 Unauthorized 403 クライアントが身元を証明していない。WWW-Authenticateを送信する
認証済みだが権限が不足 403 Forbidden 401 身元は確認済みだがアクセスを拒否した。再認証では解決しない
リソースが存在しない、または存在を確認させたくない 404 Not Found 410 安全なデフォルト。不正なプロービングからリソースを隠せる
リソースが意図的かつ永続的に削除済み 410 Gone 404 クライアントやクローラーに参照の削除を促す
重複キー、古いバージョン、編集の衝突 409 Conflict 400 リクエストは有効だが現在のリソース状態と衝突する
クライアントがレート制限を超過 429 Too Many Requests 503 クライアントが適切に後退できるよう、常にRetry-Afterを含める
コード内の未処理例外 500 Internal Server Error 502 サーバー自体が失敗した
アップストリームがゲートウェイに不正なデータを返した 502 Bad Gateway 500 障害はエッジではなくダウンストリームにある
サーバーが過負荷またはメンテナンス中 503 Service Unavailable 500 定義上、一時的な障害。可能であればRetry-Afterを追加する
アップストリームがタイムアウトした 504 Gateway Timeout 500 遅い依存関係と壊れたコードを区別できる

特に重要なのは次の2点です。

  1. 401と403はセキュリティ境界であり、スタイルの問題ではない 未認証の呼び出し元に403を返すと、リソースの存在を推測される可能性があります。
  2. 429にはRetry-Afterを付ける ヘッダーがなければ、クライアントはタイトループで再試行する可能性があります。

レート制限のヘッダー計算やアルゴリズムについては、APIレート制限の実装方法も参照してください。

2. RFC 9457 Problem Detailsでエラー形式を統一する

ステータスコードを正しく設定したら、すべてのエラーで同じメディアタイプとスキーマを使います。標準的な選択肢は、application/problem+jsonで返すRFC 9457 Problem Detailsです。

Problem Detailsのコアメンバーは次の5つです。

  • type: エラーカテゴリを識別するURI
  • title: 短い人間向けの要約
  • status: HTTPステータスコード
  • detail: このリクエストで何が問題だったか
  • instance: この障害インスタンスを識別するURI

それ以外の情報は、独自の拡張メンバーとして追加します。RFC 9457解説では、各メンバー、レジストリルール、RFC 7807からの変更点を詳しく説明しています。

たとえば、決済エンドポイントのバリデーションエラーは次のように返せます。

POST /v1/payments HTTP/1.1
Content-Type: application/json

{ "amount": -1400, "currency": "USD", "source": "card_8xKt2" }

Enter fullscreen mode Exit fullscreen mode
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "リクエストの検証に失敗しました",
  "status": 422,
  "detail": "1つ以上のフィールドの検証に失敗しました。",
  "instance": "/v1/payments/requests/req_9f3c1a7b",
  "code": "PAYMENT_VALIDATION_FAILED",
  "errors": [
    {
      "field": "amount",
      "code": "AMOUNT_NOT_POSITIVE",
      "message": "amountはマイナーユニットで正の整数である必要があります"
    }
  ],
  "request_id": "req_9f3c1a7b"
}

Enter fullscreen mode Exit fullscreen mode

errors[]は拡張メンバーです。フロントエンドはこれを使って、曖昧なエラーバナーではなく、該当するフォームフィールドにエラーを表示できます。

クライアントがプログラムで処理できるように、フィールドパスはJSON Pointerまたはドットパスのどちらかに統一し、安定させてください。

また、アプリケーションのハンドラーだけでなく、フレームワークやゲートウェイが生成するエラーにも同じ形式を適用します。ハンドラーではProblem Details、ロードバランサーではHTMLという状態になると、クライアントは2種類のパーサーを実装しなければなりません。

3. 機械可読コードと人間向けメッセージを分離する

codemessageを併用するのは意図的です。両者の対象読者が異なるため、1つの文字列にまとめないでください。

機械可読コード

AMOUNT_NOT_POSITIVECURRENCY_UNSUPPORTEDIDEMPOTENCY_KEY_REUSEDのようなコードはAPI契約です。クライアントが分岐に使うため、次の条件を満たす必要があります。

  • 安定している
  • ドキュメント化されている
  • 列挙可能である
  • 意味を変更しない

クライアントに文章を解析させてはいけません。たとえば、if (message.includes("positive"))のような実装は、文言の修正を破壊的変更にしてしまいます。

人間向けメッセージ

メッセージはログを読む開発者や利用者のためのものです。何が失敗し、どう修正すればよいかを明確にします。

"invalid amount"よりも、次のようなメッセージの方が有用です。

amountはマイナーユニットで正の整数である必要があります

ローカライズする場合はメッセージだけを翻訳し、コードは変更しません。

APIコンシューマーに自律エージェントが含まれる現在、この分離はさらに重要です。LLMベースのクライアントは、構造化され自己記述的なエラーからより適切に回復できます。詳しくはAIエージェント向けAPIエラー設計を参照してください。

4. エラー応答から機密情報を除外する

エラー応答は攻撃者の偵察チャネルになります。エラーミドルウェアでは、次の情報がクライアントに届かないようにします。

  • スタックトレース、クラス名、ファイルパス
  • 生SQL、クエリ断片、ORMエラー
  • 内部ホスト名、IPアドレス、ポート、サービス名
  • ライブラリのバージョン、フレームワークのバナー
  • 例外テキストに含まれる秘密情報、トークン、接続文字列
  • ユーザーアカウントの存在有無 ログインやパスワードリセットでは、成功・失敗の応答を対称的に保つ

基本パターンは次のとおりです。

  1. 境界で例外をすべて捕捉する
  2. リクエストIDとともに完全な例外をサーバー側へ記録する
  3. 同じIDを持つ汎用Problem Detailsを返す

クライアントには次のような情報だけを返します。

{
  "detail": "内部エラーが発生しました",
  "request_id": "req_51ad0"
}
Enter fullscreen mode Exit fullscreen mode

ログには詳細な原因を残し、サポート担当者はrequest_idで両者を結び付けます。

5. エラーを再試行可能または終端として定義する

すべてのエラーは、クライアントが次に知りたい問いに答えるべきです。

もう一度試すべきか?

一般的なデフォルトは次のとおりです。

  • 429502503504: 指数バックオフとジッターを付けて再試行
  • 500: 通常は慎重な1回の再試行
  • その他の多くの4xx: 終端エラーとして再試行しない

401403404422を同じリクエストで繰り返しても、クォータを消費しログを汚すだけです。

タイムアウトには注意が必要です。クライアントがタイムアウトした後でも、サーバー側では処理が成功している可能性があります。変更を伴うエンドポイントでは冪等性キーを受け入れ、リトライによる二重請求や二重作成を防いでください。

再試行可能性は拡張メンバーで明示できます。

{
  "type": "https://api.example.com/problems/rate-limited",
  "title": "リクエストが多すぎます",
  "status": 429,
  "code": "RATE_LIMITED",
  "retryable": true,
  "retry_after_seconds": 30
}
Enter fullscreen mode Exit fullscreen mode

retryableを使えば、ステータスコードのデフォルトを上書きできます。たとえば、再試行すると状態が壊れる特定の500サブコードを終端エラーとして扱うケースです。

この仕様を一度文書化すれば、各クライアントSDKで同じバックオフ動作を実装できます。

6. 相関IDとエラー契約をバージョン管理する

後から追加すると高くつくため、次の2点を最初に決めます。

リクエストIDを全リクエストに付与する

  • 受信したX-Request-Idを利用する、または生成する
  • すべてのログ行に記録する
  • すべてのエラーボディにrequest_idとして返す
  • 分散システムではW3Cのtraceparentもサービス間で伝播する

顧客がエラーをサポートチケットに貼り付ければ、request_idによって1時間のログ調査を1回のクエリに短縮できます。

エラー契約をAPIと同じように管理する

次の変更は通常、安全です。

  • 新しい拡張メンバーの追加
  • 新しいエラーコードの追加

一方、次の変更は破壊的です。

  • errors[].fieldの名前変更
  • 既存コードの意味の変更
  • アドホック形式からProblem Detailsへの移行

type URIを安定した識別子として使い、古い型URIは永続的に維持します。新しいセマンティクスには新しい型URIを導入し、未知の拡張メンバーや未知のコードは無視して処理を継続する、とドキュメントに明記してください。この前方互換性のルールにより、v2を待たずに契約を進化させられます。

7. Apidogで全エラーパスをテストする

エラー契約は、実行しなければ腐敗します。正常系はデモで常に実行されますが、422ブランチは顧客が実際に遭遇するまで放置されがちです。

解決策は、失敗ケースをテストスイートの第一級のシナリオにすることです。Apidogでは、サーバー側とクライアント側の両方を検証できます。

サーバーサイドのテストシナリオ

エンドポイントごとに失敗ケースを作成します。

  • 認証情報なし: 401
  • 権限不足: 403
  • 負の金額: errors[0].codeAMOUNT_NOT_POSITIVE422
  • バーストトラフィック: Retry-After付きの429

Apidogのビジュアルアサーションを使えば、スクリプトなしで次を検証できます。

  • ステータスコード
  • レスポンスヘッダー
  • Problem Detailsの各フィールド
  • JSONスキーマ全体

エラー形式のずれはCIで検出し、本番環境で発見しないようにします。アサーションのパターンについては、APIアサーションガイドを参照してください。

クライアントサイドのモックサーバー

フロントエンドチームやSDKチームは、バックエンドが失敗レスポンスを生成する前から、4xx・5xxへの対応を実装できる必要があります。

Apidogのモックサーバーは、API仕様からProblem Detailsを返せます。たとえば次のケースをシミュレートできます。

  • Retry-After: 120付きの503
  • 二重送信時の409
  • errors[]を含む完全なバリデーションエラー

これにより、クライアントの表示や再試行動作を確認できます。手作業のExpressスタブや、バックエンドコードを一時的にコメントアウトする必要はありません。

エラー契約を設計し、シナリオとモックとして定義し、両方をCIに組み込みましょう。Apidogをダウンロードし、無料で試せます。既存のOpenAPI仕様をインポートするだけで、数分でモック可能なエラー応答を作成できます。

FAQ

バリデーションエラーには400と422のどちらを使うべきですか?

リクエスト形式が不正でサーバーが理解できない場合は400を使います。無効なJSON、誤ったContent-Type、必須フィールドの欠落などが該当します。

リクエストを解析できるものの、値がドメインルールに違反する場合は422を使います。負の支払い金額や未対応の通貨などです。

422は「データを修正してください」、400は「リクエスト形式を修正してください」という診断情報を伝えられます。どちらを選ぶ場合も、すべてのエンドポイントで一貫させてください。

application/problem+jsonとは何ですか?

RFC 9457が定義する、HTTP API向けProblem Detailsの標準メディアタイプです。このContent-Typeの応答には、typetitlestatusdetailinstanceと、errors[]のような独自拡張を含められます。

登録済みのメディアタイプを使うことで、汎用クライアントやミドルウェアはカスタム設定なしでエラーを認識できます。詳しくはRFC 9457解説を参照してください。

クライアントはどのHTTPエラーを自動的に再試行すべきですか?

429502503504は、指数バックオフとジッターを付けて再試行します。Retry-Afterがあれば、その値に従ってください。

500は、慎重な1回の再試行に限定します。その他の4xxは通常、再試行しません。同じリクエストは毎回同じように失敗するためです。

変更を伴うエンドポイントでは、冪等性キーと組み合わせて、リプレイによる二重請求や二重作成を防ぎます。

バックエンドを壊さずにAPIエラー応答をテストするには?

エラーをシミュレートします。

クライアントをApidogモックサーバーに接続し、仕様から正確な4xx・5xxレスポンスを返して、表示と再試行動作を確認します。サーバー側では、無効なペイロード、認証情報の欠落、バーストトラフィックを送信するテストシナリオを作成し、ステータスコード、ヘッダー、エラーボディのスキーマを検証します。

両方をCIで実行すれば、手動で失敗を強制しなくても、エラー契約を維持できます。

Top comments (0)