RFC 9457「Problem Details for HTTP APIs」は、HTTP APIのエラー応答に機械可読な詳細を載せるためのIETFの仕様で、ステータスはProposed Standardです。2023年7月に発行され、2016年3月のRFC 7807を廃止しました。メディアタイプ application/problem+json と5つの標準メンバー(type・title・status・detail・instance)は7807から変わっていないため、7807準拠の応答はそのまま使えます。ただし9457で増えた規範(値の型が違うメンバーの無視など)は、受信側の実装で確認が要ります。この記事では、両RFCの原文を機械的に突き合わせた差分、OpenAPIでの定義、Spring Boot 4.1・ASP.NET Core 10・Express 5・Echo v5の既定値の違いを、手元で実行した結果とともに整理します。
まとめ:RFC 9457で押さえる要点
- RFC 9457はRFC 7807の改訂版で、JSONの形は同じです。Appendix Dが挙げる変更点は「問題タイプのIANAレジストリ新設」「複数の問題の扱いの明確化」「参照できないtype URIの指針」の3つです。
- Appendix Dに書かれていない規範の追加もあります。値の型が仕様と違うメンバーは無視する(MUST)、typeとinstanceは絶対URIを推奨(RECOMMENDED)の2つで、本文のRECOMMENDEDは7807の0回から3回に増えました。セキュリティの章は語句の修正だけです。
- 標準メンバーに必須のものはありません。typeを省略すると
about:blank(HTTPステータス以上の意味を持たない)とみなされます。クライアントはtypeで分岐し、知らない拡張メンバーは無視します(MUST)。 - 同じ種類の入力エラーが複数あれば、RFCの例にならって
errors配列とJSON Pointerで返せます。種類の違う問題が同時に起きたら最も関連する1件だけを返します。 - Spring Bootは
spring.mvc.problemdetails.enabledが既定でfalse、ASP.NET Coreはtypeに仕様書の節URLとtraceIdを自動で入れ、Echoの組み込みハンドラは拡張メンバーを出せません。同じ「RFC 9457対応」でも出力は揃いません。
RFC 9457の発行情報とRFC 7807との関係
RFC 9457はIETFのhttpapi WG(Building Blocks for HTTP APIs)で、draft-ietf-httpapi-rfc7807bis として改訂されました。RFC 9457の原文の冒頭には「Obsoletes: 7807」とあり、7807は廃止(Obsolete)扱いです。RFCの番号・ステータス表記の読み方はプロトコルとRFCの読み方の解説、IETFでRFCが決まるまでの手続きはIETFの標準化プロセスの解説で扱っています。
| 項目 | RFC 7807 | RFC 9457 |
|---|---|---|
| 発行 | 2016年3月 | 2023年7月 |
| 著者 | M. Nottingham, E. Wilde | M. Nottingham, E. Wilde, S. Dalal |
| ステータス | Obsolete(9457により廃止) | Proposed Standard |
| メディアタイプ | application/problem+json・+xml | 同じ(登録の参照先を9457へ更新) |
| 問題タイプのレジストリ | なし | IANA「HTTP Problem Types」 |
仕様の目的は、APIごとに独自のエラー形式を発明させないことです。RFCは、既存のHTTPステータスコードの意味を書き換えたくなる誘惑を避けるためだとも書いています。エラー形式を新設するなら、まずこの形式に乗せられないかを検討します。
application/problem+jsonの構造と5つの標準メンバー
残高不足のレスポンス例とContent-Type
Problem DetailsはJSONオブジェクトで、応答の Content-Type に application/problem+json を付けます。XML版の application/problem+xml もありますが、JSONのAPIではJSONを使います。
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: ja
{
"type": "https://api.example.com/problems/out-of-credit",
"title": "残高が不足しています",
"status": 403,
"detail": "現在の残高は30ですが、この購入には50が必要です",
"instance": "/account/12345/msgs/abc",
"balance": 30
}
RFC 9457の3章の例では、クライアントが Accept に application/problem+json を並べていなくてもサーバーはこの形式で応答しています。HTTPのコンテンツネゴシエーション上それが許されると本文に明記されているので、Acceptの指定は必須ではありません。title と detail の言語は Accept-Language で交渉でき、日本語で返すことも仕様どおりです。
type・title・status・detail・instanceの役割と省略時の扱い
| メンバー | 型 | 省略時 | 用途 |
|---|---|---|---|
| type | URI参照(文字列) | about:blank | 問題の種類の主識別子 |
| title | 文字列 | なし | 種類の短い要約 |
| status | 数値(整数) | なし | HTTPステータスの写し |
| detail | 文字列 | なし | 今回の発生に固有の説明 |
| instance | URI参照(文字列) | なし | 今回の発生の識別子 |
- typeが識別子の本体:クライアントはtype URIを主識別子として使わなければなりません(MUST)。titleは同じ種類なら発生ごとに変えない(SHOULD NOT、ローカライズは例外)ので、分岐に使うならtypeです。
- statusは参考値:statusは「advisory」で、生成側は実際のHTTPステータスと同じ値にしなければなりません(MUST)。プロキシなど汎用ソフトはHTTPステータスしか見ないため、ずれるとクライアントとインフラで判断が割れます。
- detailは人間向け:デバッグ情報でなく、クライアントが問題を直す助けになる説明を書きます。機械が読む値は拡張メンバーに出します。
- instanceは参照できなくてもよい:参照可能なURIならその問題の詳細を取得でき、参照できなければサーバー側だけが意味を知る一意な識別子になります。ログの相関IDを入れる運用は後者に当たります。
メンバーの値の型が仕様と違う場合、そのメンバーは存在しないものとして無視されます(MUST)。"status": "403" と文字列で返すと、受け手はstatusを読み飛ばします。
RFC 7807からの変更点と既存実装の互換性
Appendix Dに挙がる変更点3つ
RFC 9457のAppendix D「Changes from RFC 7807」に並ぶ変更点は3項目です。どれも指針の追加で、JSONの形やメンバーの意味は変わっていません。
| 観点 | RFC 7807 | RFC 9457 |
|---|---|---|
| 問題タイプの共有 | 規定なし | IANAレジストリ(4.2節) |
| 複数の問題 | 拡張で複数を表現。異なるステータスには207も提示 | 異なるtypeは最も関連する・緊急の1件を推奨(3章) |
| 参照できないtype URI | 規定なし | tag URI等を許容しつつ参照可能を推奨(3.1.1) |
| 検証エラーの例 | 400+invalid-params(name・reason) | 422+errors(detail・pointer) |
| XML名前空間 | urn:ietf:rfc:7807 | urn:ietf:rfc:7807(変更なし) |
表の下2行はAppendix Dに載っていない、例とXMLの差です。7807の検証エラー例の invalid-params はハイフンを含み、両RFCに同じ文で書かれた拡張名の推奨規則(英字・数字・アンダースコアのみ)に合っていませんでした。9457では規則どおりの errors に差し替わり、場所の指定もフィールド名からJSON Pointerに変わっています。XML名前空間は番号が変わっても urn:ietf:rfc:7807 のままなので、XML版を書き換える必要はありません。
Appendix Dに載らない規範の追加と、変わっていないセキュリティの章
両RFCのテキスト版を取得し、BCP 14の規範語(MUST・SHOULD・RECOMMENDEDなど)を含む文を機械的に比較すると、Appendix Dの3項目以外にも規範が増えています。冒頭の定型文を除いた本文で、RECOMMENDEDは7807の0回に対して9457は3回です。
| 9457で増えた規範 | 場所 | 強さ |
|---|---|---|
| 値の型が違うメンバーは無視する | 3.1節冒頭 | MUST |
| 異なるtypeの問題は最も関連する1件を返す | 3章 | RECOMMENDED |
| typeは絶対URI、相対ならパス全体 | 3.1.1 | RECOMMENDED |
| instanceは絶対URI、相対ならパス全体 | 3.1.5 | RECOMMENDED |
| type URIの自動取得禁止に開発ツールの例外 | 3.1.1 | SHOULD NOT(緩和) |
「型が違えば無視」は受け手の実装に効きます。7807時代に "status": "422" を文字列で返していたAPIは、9457準拠のクライアントからはstatus無しに見えます。絶対URIの推奨の理由は後述の「type URIの設計」で扱います。
逆に、5章のSecurity Considerationsは7807とほぼ同文です。違いは「thereby」の削除、「modified」から「changed」への置き換え、最終段落の主語を「Generic HTTP software」に書き直した点だけで、XSSや instance の扱いに触れる記述は増えていません。「9457でセキュリティ要件が強化された」と説明する解説もありますが、原文を並べると根拠はありません。
結論として、7807から応答形式を全面変更する必要はありませんが、受信側が型不一致メンバーを無視する義務などは確認が必要です。見直す価値があるのは、statusやtypeを想定外の型で返している箇所、typeやinstanceに短い相対URIを使っている箇所、種類の違うエラーを1つの応答に詰めている箇所の3つです。
拡張メンバーで複数エラーを返す設計
入力検証エラーをerrors配列とJSON Pointerで返す例
RFC 9457の3章は、同じ種類の問題が複数ある場合の例として次の形を示しています。errors は仕様が定めたメンバーではなく、この問題タイプが定義した拡張メンバーです。
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Your request is not valid.",
"errors": [
{ "detail": "must be a positive integer", "pointer": "#/age" },
{ "detail": "must be 'green', 'red' or 'blue'", "pointer": "#/profile/color" }
]
}
pointer はリクエスト本文の中の位置をJSON Pointer(RFC 6901)で示します。ネストしたフィールドを #/profile/color のように一意に指せるので、フォームの入力欄にエラーを戻す処理がフィールド名の命名規則に依存しなくなります。
400と422の選び方
RFC 9457自体はステータスコードを選ばせる規定を持たず、例が7807の400から422に変わっただけです。選び分けの根拠はRFC 9110の15.5.21節にあり、422は「Content-Typeを理解でき、構文も正しいが、中身を処理できない」場合のコードです。JSONとして壊れている本文は400、JSONとしては正しいが年齢が負数といった値の不備は422、と分けるとクライアントの再送判断が単純になります。なお、9110で422の名称は「Unprocessable Content」に改められましたが、ASP.NET Core 10の既定titleは旧名の「Unprocessable Entity」のままです。
異なる問題タイプの同時発生時における応答の優先順位
種類(type)が異なる複数の問題に当たった場合、RFC 9457は最も関連が深いか緊急度の高い1件を返すことを推奨しています(RECOMMENDED)。異種の問題を束ねる「batch」型の問題タイプも作れますが、HTTPの意味論にうまく対応しないと明記されています。認証切れと入力不備が同時にあるなら先に401を返し、入力不備は再送時に422で返す、という順で設計します。
拡張メンバー名の規則とクライアントの無視義務
拡張メンバー名は英字で始め、英字・数字・アンダースコアだけで3文字以上にすることが推奨されています(SHOULD)。XML版に変換したときにXMLの名前規則を満たすためです。trace-id より trace_id や traceId を選ぶ理由はここにあります。クライアントは知らない拡張メンバーを無視しなければならない(MUST)ので、サーバーは後から拡張メンバーを足しても互換性を壊しません。既存の拡張メンバーの意味や型を変えるのは破壊的変更です。
type URIの設計とIANA「HTTP Problem Types」レジストリ
about:blankを使う条件とtitleの決め方
typeを省略した応答や "type": "about:blank" の応答は、HTTPステータスコード以上の意味を持たない問題を表します。このときのtitleは、そのステータスの推奨フレーズ(404なら「Not Found」)と同じにすることが推奨され、ローカライズは許されています。残高不足や在庫切れのような業務固有の意味を伝えたいなら、about:blankでなく独自のtype URIを発行します。
絶対URIを推奨する理由と参照できないURIの扱い
typeとinstanceは相対URIも書けますが、9457は絶対URIを推奨しています。相対URIは応答したリソースのURIを基準に解決されるため、/foo/bar/123 と /widget/456 が同じ example-problem を返すと、解決後は別々のURIになるからです。相対URIを使うなら /types/123 のようにパス全体を書きます。
tag:[email protected],2021-09-17:OutOfLuck のような参照できないURIも許されていますが、RFCは参照可能なURIを勧めています。後から参照可能なURIへ切り替えると識別子そのものが変わり、クライアントにとって破壊的変更になるためです。httpsのtype URIには問題の解決方法を説明するHTMLを置くのが推奨です(SHOULD)。一方クライアントは、開発ツールで情報を見せる場合を除き、type URIを自動で取得しに行ってはいけません(SHOULD NOT)。
IANAに登録済みの問題タイプ6件(2026年6月26日更新)
RFC 9457が新設したIANAの「HTTP Problem Types」レジストリは、登録方針がSpecification Required(公開された仕様書が必要)で、アプリ固有の値は登録できません。2026年10月25日時点の登録は次の6件で、最終更新は2026年6月26日です。
| Type URI(末尾) | 名称 | 推奨ステータス | 出典 |
|---|---|---|---|
| about:blank | See HTTP Status Code | N/A | RFC 9457 |
| #date | Date Not Acceptable | 400 | RFC 9458 6.5.2 |
| #ohttp-key | Oblivious HTTP key configuration not acceptable | 400 | RFC 9458 5.3 |
| #digest-unsupported-algorithms | Unsupported Hashing Algorithms | 400 | digest-fields-problem-types |
| #digest-invalid-values | Invalid Digest Values | 400 | digest-fields-problem-types |
| #digest-mismatched-values | Mismatched Digest Values | 400 | digest-fields-problem-types |
about:blank以外の5件は https://iana.org/assignments/http-problem-types# を前置したURIです。RFC 9458はOblivious HTTPの仕様です。digest系の3件の出典 draft-ietf-httpapi-digest-fields-problem-types-06 は、Datatrackerで2026年10月25日時点もRFC Editorの編集待ちで、RFC番号は付いていません。登録済みの型はプロトコル仕様向けに限られ、一般的な業務APIが流用できるものはほぼありません。業務エラーは自社ドメインのtype URIで定義します。
OpenAPIでの定義とAppendix AのJSON Schema
OpenAPI 3.1のcomponentsによる共通スキーマ定義
OpenAPI 3.1はJSON Schema Draft 2020-12に揃ったので、RFC 9457のAppendix AにあるスキーマをほぼそのままAPI定義へ移せます。共通の ProblemDetails を定義し、問題タイプごとに allOf で拡張メンバーを足す形にすると、エラー応答の型をクライアント生成ツールにも渡せます。次の定義は openapi-spec-validator 0.9.0 で検証してエラーが出ないことを確認しました(掲載では必須の info 欄を省略しています)。
openapi: 3.1.0
paths:
/purchases:
post:
responses:
'422':
description: 入力検証エラー
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ValidationProblem'
components:
schemas:
ProblemDetails:
type: object
properties:
type: { type: string, format: uri-reference, default: about:blank }
title: { type: string }
status: { type: integer, minimum: 100, maximum: 599 }
detail: { type: string }
instance: { type: string, format: uri-reference }
ValidationProblem:
allOf:
- $ref: '#/components/schemas/ProblemDetails'
- type: object
required: [type, errors]
properties:
type:
const: https://api.example.com/problems/validation-error
errors:
type: array
items:
type: object
required: [detail, pointer]
properties:
detail: { type: string }
pointer: { type: string }
この ValidationProblem に対し、前章のRFCの422の例はjsonschema 4.25.1で検証が通り、errors の要素を {"field": ..., "message": ...} の独自形式にした応答は「detail・pointerが無い」として弾かれました。社内APIで旧形式と新形式が混ざっていないかを、テストで機械的に見つけられます。
Appendix Aのスキーマにおける必須項目・URI形式・整数値の検証
Appendix AのスキーマはRFC自身が「非規範(non-normative)で、本文と食い違えば本文が優先」と断っています。jsonschema 4.25.1(Draft 2020-12)で応答例を検証し、必須項目、URI形式検査、数値の整数判定の挙動を確認しました。
| 入力 | Appendix Aでの結果 | 理由 |
|---|---|---|
| {}(空オブジェクト) | 通る | requiredが無い |
| “type”: “not a uri” | 通る | formatは既定で検査されない |
| “status”: 403.0 | 通る | 小数部0は整数扱い |
| “status”: “403” | 弾かれる | 型が文字列 |
| “status”: 600 | 弾かれる | maximum 599を超える |
空オブジェクトが通るのは仕様どおり(全メンバー任意)ですが、自社APIで「typeとtitleは必ず入れる」と決めているなら、スキーマ側に required を足す必要があります。format: uri-reference は、Pythonのjsonschemaでは FormatChecker を指定しても追加パッケージが無い状態では検査されませんでした。URIの形式まで保証したいなら、検証器の設定とパッケージを明示します。なお、スキーマの title は9457になった今も「An RFC 7807 problem object」のままです。
主要フレームワークのRFC 9457対応と既定値の違い
同じ「RFC 9457対応」でも、フレームワークごとに有効化の方法とtypeの既定値が違います。複数言語のサービスが同じAPIゲートウェイの後ろに並ぶ構成では、この差がそのままクライアントから見える揺れになります。
| フレームワーク | 確認した版 | 有効化 | typeの既定 | 拡張メンバー |
|---|---|---|---|---|
| Spring Framework / Boot | 7.0.10 / 4.1.2 | プロパティ(既定false) | 未設定(about:blank扱い) | setPropertyで可 |
| ASP.NET Core | 10.0.13 | AddProblemDetails() | 仕様書の節URL | traceIdを自動付与 |
| Express | 5.3.0 | 組み込みなし | 自前で決める | 自由 |
| Echo(Go) | v5.4.2 | ProblemDetailsHTTPErrorHandler | about:blank | 組み込みでは不可 |
Spring Boot:problemdetailsプロパティとResponseEntityExceptionHandler
Spring Frameworkのリファレンス「Error Responses」によると、Spring Frameworkは ProblemDetail・ErrorResponse・ResponseEntityExceptionHandler でRFC 9457を扱い、Spring MVCの組み込み例外はすべて ErrorResponse を実装しています。Spring Bootでは次のプロパティで組み込み例外がProblem Details形式になります。v4.1.2のソースでも初期値を持たないbooleanで、既定はfalseです。
spring.mvc.problemdetails.enabled=true
このままだと入力検証エラー(MethodArgumentNotValidException)のdetailは「Invalid request content.」だけで、どのフィールドが悪いかが出ません。ResponseEntityExceptionHandler を継承したハンドラで errors を足します。次のコードはSpring Framework 7.0.10のjarに対してコンパイルを確認しました。import文と独自例外の定義は省略しています。OutOfCreditExceptionはRuntimeExceptionを継承し、公開可能なメッセージと残高を返すgetBalance()を持つ想定です。
@RestControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
ProblemDetail pd = ex.getBody();
pd.setType(URI.create("https://api.example.com/problems/validation-error"));
pd.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
.map(e -> Map.of(
"detail", String.valueOf(e.getDefaultMessage()),
"pointer", "#/" + e.getField().replaceAll("\\[(\\d+)\\]", ".$1").replace('.', '/')))
.toList());
return handleExceptionInternal(ex, pd, headers, status, request);
}
@ExceptionHandler(OutOfCreditException.class)
ProblemDetail handleOutOfCredit(OutOfCreditException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, ex.getMessage());
pd.setType(URI.create("https://api.example.com/problems/out-of-credit"));
pd.setTitle("You do not have enough credit.");
pd.setProperty("balance", ex.getBalance());
return pd;
}
}
setProperty で入れた値は、Jacksonを使う場合 ProblemDetailJacksonMixin によってトップレベルのメンバーとして出力されます。items[0].name のようなフィールドパスは #/items/0/name に変換されます。この簡易変換は、Javaのプロパティ名とJSONのキーが一致する通常のプロパティと配列添字が対象です。Mapのキーや特殊文字を含む名前には、パスの解析、JSON Pointerのエスケープ、URIフラグメントの符号化を別に実装します。
掲載例のように ResponseEntityExceptionHandler を継承したBeanがある場合、Bootの既定ハンドラは自動登録されません。これを継承しない別の @ControllerAdvice で先に例外を扱うなら、自前のハンドラをBootの自動構成(order 0)より前に並べます。@ControllerAdvice とDispatcherServletの関係はSpring MVCの解説を参照してください。
ASP.NET Core:AddProblemDetailsによるtypeとtraceIdの自動設定
ASP.NET Coreでは AddProblemDetails() で IProblemDetailsService が登録され、例外ハンドラーやステータスコードページのミドルウェアがProblem Detailsを返すようになります。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
app.MapGet("/accounts/{id}", (int id) =>
id > 0
? Results.Ok(new { id })
: Results.Problem(
type: "https://api.example.com/problems/invalid-account",
title: "Invalid account id.",
statusCode: StatusCodes.Status400BadRequest));
app.Run();
v10.0.13のProblemDetailsDefaults.csとDefaultProblemDetailsWriter.csで確認できる既定の挙動は3つあり、Springと揃いません。
- typeを指定しないと、ステータスに応じたRFC 9110の節URL(400なら
https://tools.ietf.org/html/rfc9110#section-15.5.1)が入ります。about:blankにはなりません。422だけはWebDAVのRFC 4918の節URLです。指定したtypeは上書きされません。 - 既定のライターが拡張メンバー
traceIdを付けます。 - リクエストの
Acceptが未指定・空の場合や、*/*・application/*の場合も既定のライターが応答します。application/jsonかapplication/problem+jsonを明示する必要はありません。
エラー応答のtypeに入っている tools.ietf.org/html/rfc9110#section-15.5.1 のようなURLは、ASP.NET Coreの既定値と一致します。ただし、この値だけで実装元を特定することはできません。Microsoft Learnの ASP.NET Core 10 向け「Handle errors in ASP.NET Core APIs」は本文で「RFC 7807-compliant」と表記していますが、7807と9457でJSONの形は同じなので実害はありません。
Express:エラーミドルウェアによるProblem Detailsの実装
Express 5にはProblem Details専用の組み込み機能がないので、エラー処理ミドルウェアで返します。以下は生成済みのappに追加する抜粋です。ProblemErrorを定義した後に業務ルートを登録し、その後に404ハンドラ、最後にエラーハンドラを登録します。Express 5.3.0・Node.js v26.5.0で実行し、422・403・400(JSONの構文エラー)・500・404の各応答を確認しました。
import { STATUS_CODES } from 'node:http';
class ProblemError extends Error {
constructor(status, type, title, detail, ext = {}) {
super(detail);
Object.assign(this, { status, type, title, detail, ext });
}
}
app.use((req, res) => {
res.status(404).type('application/problem+json')
.json({ type: 'about:blank', title: 'Not Found', status: 404 });
});
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
const p = err instanceof ProblemError
? { type: err.type, title: err.title, status: err.status, detail: err.detail,
...err.ext }
: Number.isInteger(err.status) && err.status >= 400 && err.status <= 499
? { type: 'about:blank', title: STATUS_CODES[err.status], status: err.status }
: { type: 'about:blank', title: 'Internal Server Error', status: 500 };
res.status(p.status).type('application/problem+json').json(p);
});
応答ヘッダーは application/problem+json; charset=utf-8 になりました。Expressがcharsetパラメーターを付けるためで、クライアント側でContent-Typeを文字列の完全一致で判定するとこの応答を取りこぼします。想定外の例外で err.message をdetailに入れない作りにしているのは、後述の情報漏えい対策のためです。
Echo(Go):ProblemDetailsHTTPErrorHandlerの出力と拡張メンバーの制約
GoのWebフレームワークEchoは、v5.4.0でRFC 9457用のエラーハンドラ ProblemDetailsHTTPErrorHandler を追加しました(pkg.go.devのAPIリファレンス)。Echo自体の特徴やGinとの比較はGo言語のEchoの解説にまとめています。次のコードをEcho v5.4.2・Go 1.27.2で実行しました。
var errOutOfCredit = &echo.ProblemError{
Type: "https://api.example.com/problems/out-of-credit",
Title: "You do not have enough credit.",
Status: http.StatusForbidden,
}
e := echo.New()
e.HTTPErrorHandler = echo.ProblemDetailsHTTPErrorHandler(false)
e.GET("/credit", func(c *echo.Context) error { return errOutOfCredit })
e.GET("/bad", func(c *echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "id must be a number")
})
/credit 403 application/problem+json {"type":"https://api.example.com/problems/out-of-credit","title":"You do not have enough credit.","status":403}
/bad 400 application/problem+json {"type":"about:blank","title":"Bad Request","status":400,"detail":"id must be a number"}
/nope 404 application/problem+json {"type":"about:blank","title":"Not Found","status":404}
Expressと違いContent-Typeにcharsetは付かず、未定義のルートもProblem Details形式で返ります。注意点は2つです。1つ目は、ProblemError 構造体が標準5メンバーのフィールドしか持たないことです。balance や errors のような拡張メンバーは、このハンドラでは出力できません。拡張が要る問題タイプでは自前のエラーハンドラを書きます。2つ目は引数の exposeError です。trueにすると、想定外のエラーの err.Error() がdetailに入り、実行では dial tcp 10.0.0.5:5432: connect: connection refused という内部のDB接続先がそのまま応答に出ました。本番はfalseにします。
クライアント側の応答処理:typeによる分岐とdetailの表示
受け手の実装では、仕様の義務と推奨が4点に効きます。メディアタイプはパラメーターを外し小文字に揃えて比較する、分岐はtypeで行う、detailは表示にだけ使う、知らない拡張は無視する、の4つです。次の関数は前節のExpressサーバーに対して実行し、422をvalidation、403をcredit、400・500・404をgenericに振り分けることを確認しました。typeが相対URIなら応答URLを基準に解決してから比べます(RFC 9457の3.1.1節で受け手の義務)。https://api.example.com/purchase から返った /problems/validation-error は https://api.example.com/problems/validation-error に解決されます。外部から来る errors は配列かどうかを確かめてから読みます。
async function handleProblem(res) {
const mediaType = (res.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();
if (mediaType !== 'application/problem+json') {
return { kind: 'unknown', status: res.status };
}
const p = await res.json();
let type = 'about:blank';
if (typeof p.type === 'string') {
try {
type = new URL(p.type, res.url).href;
} catch {
return { kind: 'unknown', status: res.status };
}
}
switch (type) {
case 'https://api.example.com/problems/validation-error':
return { kind: 'validation', fields: Array.isArray(p.errors)
? p.errors.filter((e) => e && typeof e.pointer === 'string').map((e) => e.pointer)
: [] };
case 'https://api.example.com/problems/out-of-credit':
return { kind: 'credit', balance: p.balance };
default:
return { kind: 'generic', status: res.status,
title: typeof p.title === 'string' ? p.title : undefined };
}
}
ステータスは本文の status でなく res.status を使っています。RFCは両者の食い違いについて、途中のプロキシやキャッシュがHTTPステータスを書き換えた可能性を示すもので、どちらが優先かは明確でないとしています。リトライ判定のようにインフラと挙動を揃えたい処理では、実際のHTTPステータスに合わせる方が安全です。問題タイプの定義でRetry-Afterヘッダーを使うよう指定することもRFC 9457は認めています(MAY)。リトライ間隔の設計はリトライパターンの解説にまとめています。
Problem Detailsを使わない方がよい場面と情報漏えいの注意点
RFC 9457自身が「使わなくてよい」とする場面があります。次の3つに当てはまるなら、Problem Detailsを足すより既存の手段で済ませます。
- ステータスコードだけで意味が完結する:RFCはPUTへの403 Forbiddenを例に、「書き込み禁止」という問題タイプは不要だとしています。汎用的な状況は素のステータスコードで表します(各コードの定義はRFC 9110の15章)。
- 応答がまだリソースの表現である:部分的な失敗を含む一覧の応答などは、そのアプリの形式の中で詳細を書く方が望ましいと仕様は述べています。
- 既にドメイン固有のエラー形式がある:Problem Detailsは新しいエラー形式の発明を避けるためのもので、既存の形式を置き換えるためのものではありません。既存APIに足すなら、Acceptヘッダーによるコンテンツネゴシエーションで新形式を選ばせます。
もう1つの落とし穴は情報漏えいです。RFC 9457の5章は、スタックダンプのような実装の詳細をHTTPインターフェースから見せないよう求めています。例外メッセージをそのままdetailに流すと、SQL文やファイルパス、前述のEchoの例のようにDBの接続先が外部に出ます。Spring Frameworkのドキュメントも、組み込み例外のProblem Detailsは実装の詳細を出さないようメッセージを差し替えるのがよい、としています。追跡にはinstanceやtraceIdの識別子だけを返し、詳細はサーバーのログで引きます。既存APIのエラー形式をRFC 9457へ揃える設計や移行は、API開発・システム連携の支援でも扱っています。
よくある質問
RFC 9457とRFC 7807の違いは何ですか?
JSONの形とメディアタイプは同じです。Appendix Dに挙がる変更は、問題タイプを共有するIANAレジストリの新設、複数の問題の扱いの明確化、参照できないtype URIの指針の3点です。ほかに、値の型が違うメンバーを無視する義務(MUST)と、typeとinstanceの絶対URIの推奨が本文に加わりました。7807は9457の発行(2023年7月)で廃止扱いなので、仕様書として参照するなら9457を示します。既存の応答形式は維持できますが、受信側の型不一致メンバーの扱いなど、追加規範への適合確認は必要です。
application/problem+jsonとは何ですか?
RFC 9457のProblem DetailsオブジェクトをJSONで表したときのメディアタイプです。エラー応答の Content-Type に付け、クライアントはこれを見て本文をProblem Detailsとして読みます。クライアントのAcceptに含まれていなくても返してよいと仕様に明記されています。Expressのようにcharsetパラメーターが付く実装もあるので、判定は「;」より前の値を取り出し、空白を除いて大文字・小文字を区別せず比較します。
Problem Detailsで必須の項目はありますか?
5つの標準メンバーはいずれも必須ではありません。typeを省略するとabout:blankとみなされ、HTTPステータス以上の意味を持たない問題になります。実務では、クライアントが分岐に使うtypeと、人間が読むtitleは入れておくのが無難です。その方針をOpenAPIで強制するなら、スキーマに required を足します。statusを入れる場合は実際のHTTPステータスと同じ値にする義務があります。
Spring BootでRFC 9457に対応するには?
組み込み例外だけなら spring.mvc.problemdetails.enabled=true を設定します(Spring Boot 4.1.2でも既定はfalse)。独自の業務例外は @ExceptionHandler から ProblemDetail を返し、setType と setProperty でtype URIと拡張メンバーを足します。入力検証エラーのフィールド別情報は既定では出ないので、ResponseEntityExceptionHandler の handleMethodArgumentNotValid を上書きしてerrors配列を追加します。
title・detailを日本語で返してもよいですか?
問題ありません。RFC 9457はtitleとdetailの言語を Accept-Language で交渉できるとしており、応答には Content-Language を付けます。titleは同じ種類の問題なら発生ごとに変えないのが原則ですが、ローカライズによる違いは例外として認められています。about:blankを使う場合のtitleはステータスの推奨フレーズ(「Not Found」など)に揃え、その翻訳は許されます。