RFC 9457対応のAPIエラーレスポンス設計|Problem Detailsの実装

公開:

あるエンドポイントは { "error": "Invalid input" }、別のエンドポイントは { "message": "not found", "code": 404 }——APIのエラーレスポンスは、正常系に比べて設計が後回しにされやすく、気づくとエンドポイントや担当者ごとに形がバラバラになりがちです。そのたびにフロントエンドは個別のパース処理を書き、QAはエラーケースのたびに仕様書を読み直すことになります。

この問題に対するIETFの標準が RFC 9457「Problem Details for HTTP APIs」 です。この記事では、RFC 9457の構造と設計のポイント、主要フレームワークでの実装例、そしてエラー応答をテストで再現する方法までをまとめます。

RFC 9457とは

RFC 9457は、HTTP APIのエラーの詳細を機械可読な形式でレスポンスボディに載せるための標準フォーマットです。2023年7月に公開され、2016年の RFC 7807を置き換え(obsolete) ました。JSONの構造はRFC 7807と互換なので、RFC 7807対応をうたうライブラリの出力はそのままRFC 9457の形式として扱えます。

レスポンスの Content-Type には専用のメディアタイプ application/problem+json(XMLなら application/problem+xml)を使います。クライアントは Content-Type を見るだけで「これはProblem Details形式のエラーだ」と判別できます。

独自のエラースキーマを社内で設計し直すのではなく標準に乗ることで、フロントエンドは共通のエラーハンドリングを1つ書けば済み、QAも「エラー時は必ずこの形で返る」という前提でテストを設計できるようになります。

Problem Detailsの5つの標準メンバー

RFC 9457が定義する標準メンバーは次の5つです。すべて任意ですが、実務では type・title・status・detail の4つを埋めるのが一般的です。

メンバー 役割
type 問題の種類を識別するURI。省略時は about:blank として扱われる
title 問題の種類を表す短い要約。同じ type なら発生のたびに変えない
status HTTPステータスコード。実際のレスポンスのステータスと必ず一致させる
detail 今回の発生に固有の、人間が読める説明
instance 今回の発生そのものを識別するURI。ログやトレースとの紐付けに使える

押さえておきたいのは、status はあくまで参考情報だという点です。RFC 9457は、生成側が実際のHTTPレスポンスと同じステータスコードを使うことを必須(MUST)としています。Problem Detailsを理解しない汎用のHTTPソフトウェアは、ボディではなくHTTPステータスを見て動くからです。

また、type を省略した(about:blank の)場合、title はそのステータスコードの標準的な名前(404なら "Not Found")と同じにすることが推奨されています。

拡張メンバーでエラーの詳細を伝える

RFC 9457は、標準メンバーに加えてアプリケーション固有の 拡張メンバー を追加することを認めています。クライアントは認識できない拡張メンバーを無視しなければならない(MUST)とされているため、後方互換性を保ったままフィールドを増やせます。

実務で最もよく使うのが、バリデーションエラーをまとめて返す errors 配列です。RFC 9457自身も、この形を例として示しています。

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Your request is not valid.",
  "status": 422,
  "detail": "リクエストボディに不正な値が含まれています。",
  "errors": [
    { "pointer": "#/quantity", "detail": "1以上の整数を指定してください" },
    { "pointer": "#/shippingAddress/country", "detail": "翌日配送は国内住所のみ対応しています" }
  ]
}

pointer はJSON Pointer(RFC 6901)でリクエスト内の問題箇所を指します。フロントエンドはこれを使って、フォームの該当フィールドの横にエラーメッセージを出し分けられます。

レート制限の429では、Retry-After ヘッダーに加えて再試行までの秒数を拡張メンバーで持たせておくと、クライアント側のリトライ処理が書きやすくなります。

{
  "type": "https://api.example.com/problems/rate-limit-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "1分間のリクエスト上限(60回)を超えました。",
  "retryAfterSeconds": 30
}

RFC 7807からの変更点

すでにRFC 7807でエラー設計をしているチーム向けに、RFC 9457で変わった点を整理します。RFC 9457の付録Dが挙げている変更は次の3つです。

  1. よく使われる問題種別のレジストリ(HTTP Problem Types)を新設した。 IANAのレジストリで、RFC 9457自身が登録した about:blank のほか、2026年9月時点で6件が登録されています。まだ数は少ないものの、業界横断で使える汎用的な問題種別を増やしていくための仕組みです
  2. 複数の問題の扱いを明確にした。 同じ種類の問題が複数ある場合は、上の errors のように1つの type の中で拡張メンバーとして並べます。一方、種類の異なる問題が同時に起きた場合は、最も重要・緊急な1つだけをレスポンスとして返すことが推奨されています。種類の違う問題を1つのレスポンスに詰め込む「バッチ」型は、HTTPのセマンティクスに合わないためです
  3. 参照できない type URIの使い方を示した。 tag URIのような、アクセスできない識別子としてのURIも使ってよいと明記されました。ただし仕様としては、将来ツールでURIを解決したくなったときに type を変える(=破壊的変更になる)ことを避けるため、アクセスできるURIの方を推奨しています。なお、クライアントはデバッグ用途などを除いて type のURIに自動でアクセスすべきではない(SHOULD NOT)とされています

実装例

どのフレームワークでも共通するコツは、エラーレスポンスを組み立てる場所を1か所に集約することです。個々のハンドラーでJSONを組み立てず、例外やエラーオブジェクトを投げて、共通のエラーハンドラーがProblem Detailsに変換する構成にします。

ASP.NET Core

ASP.NET Coreは.NET 7から AddProblemDetails() を提供しており、未処理の例外やステータスコードだけのレスポンスをProblem Details形式に変換できます。

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] =
            context.HttpContext.TraceIdentifier;
    };
});

app.UseExceptionHandler();
app.UseStatusCodePages();

コントローラーでは Problem() や ValidationProblem() を返すだけで、フィールドごとのエラーを含むレスポンスになります。

Node.js(Express)

Expressでは、共通のエラーハンドリングミドルウェアで変換するのが定石です。

app.use((err, req, res, next) => {
  const status = err.status || 500;
  res.status(status)
     .type('application/problem+json')
     .json({
       type: err.type || 'about:blank',
       title: err.title || 'Internal Server Error',
       status,
       detail: err.detail,
       instance: req.originalUrl,
     });
});

res.json() は Content-Type が未設定のときだけ application/json を設定するため、先に type() で指定したメディアタイプがそのまま使われます。

Node.js(Fastify)

Fastifyでは setErrorHandler に集約します。

app.setErrorHandler((err, request, reply) => {
  const status = err.statusCode ?? 500;
  reply
    .code(status)
    .type('application/problem+json')
    .send({
      type: 'about:blank',
      title: status >= 500 ? 'Internal Server Error' : err.message,
      status,
      instance: request.url,
    });
});

500系では err.message をそのまま返さないようにしている点に注目してください。理由は次の節で説明します。

導入時の注意点

detail に内部情報を載せない。 RFC 9457のセキュリティの節でも、スタックトレースのような実装の詳細を公開しないよう注意を促しています。SQLのエラーメッセージや内部のパスをそのまま detail に入れず、利用者向けの説明と、ログに残す詳細は分けて設計します。

type の値をカタログ化する。 validation-error、rate-limit-exceeded のように問題の種類ごとの識別子と命名規則を決め、OpenAPIやドキュメントに一覧としてまとめておきます。フロントエンドは type ごとの処理を対応表として書けるようになります。RFC 9457は、type には可能な限り絶対URIを使うことを推奨しています。

既存クライアントへの影響を確認する。 Content-Type を application/json から application/problem+json に変えると、既存のクライアントがパースに失敗することがあります。新しいエンドポイントやバージョンを上げたAPIから段階的に適用するのが安全です。

開発チームより

これから新しくAPIを開発するなら、RFC 9457を前提にエラー形式を最初から統一しておくことをおすすめします。一方、すでに稼働しているシステムにRFC 9457を後から導入するかどうかは、既存クライアントの改修を含めた開発コストと、統一によって得られるメリットを比べて判断することになると考えています。

実は、ScenarioMockの管理APIのエラーレスポンスはRFC 9457ではなく、次のような独自の形式です。

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": []
  }
}

独自の形式でも、この記事で挙げた考え方は取り入れています。エラーレスポンスはFastifyの setErrorHandler の1か所で組み立て、プログラムが判定するための code と、人が読むための message を分けています。500系のエラーでは内部のメッセージを返さずに Internal server error とだけ返し、詳細はサーバー側のエラー監視に送っています。

一方で、独自の形式では、type による問題の種類の共有や、Problem Detailsに対応したライブラリやツールの恩恵は受けられません。RFC 9457に準拠すれば、「形式を1つに決めて、1か所で組み立てる」ことを、業界共通の語彙のまま実現できます。これが、新しく開発するAPIにはRFC 9457をおすすめする理由です。

ScenarioMockでProblem Detailsのエラー応答を再現する

エラーレスポンスの形式を決めても、422や429を実際のバックエンドで意図的に起こしてテストするのは手間がかかります。テストデータの仕込みが必要なうえ、CIでは再現が不安定になりがちです。ScenarioMock では、次の手順でProblem Detailsを返すモックAPIを作れます。

  1. プロジェクトを作成し、エンドポイント(例: POST /orders)を追加する
  2. 固定のレスポンスとして、ステータス 422、ヘッダー {"Content-Type": "application/problem+json"}、ボディに上の errors を含むJSONを設定する
  3. シーケンスのシナリオを使うと、「1回目のリクエストは422、2回目以降は200」のように回数で応答を切り替えられる。修正して再送したときの画面遷移まで確認できる
  4. 条件分岐のシナリオを使うと、リクエストのヘッダーやボディの値に応じて、422と429を出し分けられる
  5. 作ったモックのURLをフロントエンドやQAの担当者に共有する

バックエンドのRFC 9457対応と並行して、フロントエンドのエラー表示やリトライ処理を先に作り込めるのが大きな利点です。モックAPIそのものの考え方は モックAPIとは?必要な理由から作り方、無料ツールの選び方まで で詳しく解説しています。

まとめ

まずは新しく作るエンドポイント1つから、application/problem+json で返すところから始めてみてください。

設計したProblem Detailsのエラー応答を、バックエンドの実装を待たずにフロントエンドやQAと共有してみませんか。ScenarioMockなら422や429を返すモックAPIを数分で作れます。

エラー応答のモックを無料で作る