---
title: "RFC 9457（Problem Details）とは？application/problem+jsonの項目とRFC 7807との違い・実装例"
url: "https://www.issoh.co.jp/tech/details/10819/"
published: 2026-02-02
updated: 2026-10-25
categories: ["REST API"]
publisher: "株式会社一創"
---

# RFC 9457（Problem Details）とは？application/problem+jsonの項目とRFC 7807との違い・実装例

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の原文](https://www.rfc-editor.org/rfc/rfc9457.html)の冒頭には「Obsoletes: 7807」とあり、7807は廃止（Obsolete）扱いです。RFCの番号・ステータス表記の読み方は[プロトコルとRFCの読み方の解説](/tech/details/17591/)、IETFでRFCが決まるまでの手続きは[IETFの標準化プロセスの解説](/column/details/7573/)で扱っています。

| 項目          | 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」](https://www.rfc-editor.org/rfc/rfc9457.html#appendix-D)に並ぶ変更点は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節](https://www.rfc-editor.org/rfc/rfc9110.html#section-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:example@example.org,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」レジストリ](https://www.iana.org/assignments/http-problem-types/http-problem-types.xhtml)は、登録方針が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](https://datatracker.ietf.org/doc/draft-ietf-httpapi-digest-fields-problem-types/)で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」](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html)によると、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の解説](/tech/details/10533/)を参照してください。

### 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](https://github.com/dotnet/aspnetcore/blob/v10.0.13/src/Shared/ProblemDetails/ProblemDetailsDefaults.cs)と[DefaultProblemDetailsWriter.cs](https://github.com/dotnet/aspnetcore/blob/v10.0.13/src/Http/Http.Extensions/src/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リファレンス](https://pkg.go.dev/github.com/labstack/echo/v5#ProblemDetailsHTTPErrorHandler)）。Echo自体の特徴やGinとの比較は[Go言語のEchoの解説](/tech/details/3399/)にまとめています。次のコードを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）。リトライ間隔の設計は[リトライパターンの解説](/tech/details/16185/)にまとめています。

## Problem Detailsを使わない方がよい場面と情報漏えいの注意点

RFC 9457自身が「使わなくてよい」とする場面があります。次の3つに当てはまるなら、Problem Detailsを足すより既存の手段で済ませます。

- **ステータスコードだけで意味が完結する**：RFCはPUTへの403 Forbiddenを例に、「書き込み禁止」という問題タイプは不要だとしています。汎用的な状況は素のステータスコードで表します（各コードの定義は[RFC 9110の15章](https://www.rfc-editor.org/rfc/rfc9110.html#section-15)）。
- **応答がまだリソースの表現である**：部分的な失敗を含む一覧の応答などは、そのアプリの形式の中で詳細を書く方が望ましいと仕様は述べています。
- **既にドメイン固有のエラー形式がある**：Problem Detailsは新しいエラー形式の発明を避けるためのもので、既存の形式を置き換えるためのものではありません。既存APIに足すなら、Acceptヘッダーによるコンテンツネゴシエーションで新形式を選ばせます。

もう1つの落とし穴は情報漏えいです。RFC 9457の5章は、スタックダンプのような実装の詳細をHTTPインターフェースから見せないよう求めています。例外メッセージをそのままdetailに流すと、SQL文やファイルパス、前述のEchoの例のようにDBの接続先が外部に出ます。Spring Frameworkのドキュメントも、組み込み例外のProblem Detailsは実装の詳細を出さないようメッセージを差し替えるのがよい、としています。追跡にはinstanceやtraceIdの識別子だけを返し、詳細はサーバーのログで引きます。既存APIのエラー形式をRFC 9457へ揃える設計や移行は、[API開発・システム連携の支援](/service/system/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」など）に揃え、その翻訳は許されます。

## 関連記事

- [APIエラーレスポンス設計とは？コード体系・リトライ判断・漏洩防止の実装手順](/tech/details/17229/)
- [REST API設計のベストプラクティス｜URI・冪等性・エラー応答のレビュー基準](/tech/details/17225/)
- [API設計とは？リソース定義からURI・スキーマ・エラー・版管理までの実装手順](/tech/details/17223/)
- [HTTP error 400（Bad Request）とは？発生源の切り分けとnginx・Apache・IIS別の直し方](/tech/details/17397/)
- [Spring Bootとは｜4.1系の依存名の変更と最小Web APIの作り方](/tech/details/17478/)

---

出典: [RFC 9457（Problem Details）とは？application/problem+jsonの項目とRFC 7807との違い・実装例](<https://www.issoh.co.jp/tech/details/10819/>)（株式会社一創）
