思ってることってこんなもんだよ
HTTP APIの詳細なエラー情報をレスポンスに持たせるための仕様

今日では HTTP(s) で API が公開されることは当たり前の時代ですが、エラーをアプリケーションにどう伝えるかは、個々の API の設計に依存していました。特に、HTTP ステータスコードは有限であり、元々持っている意味があるので、自由に使うことはできません。API はそのドメインごとにもっと複雑で細かなエラー情報があるはずで、それらはレスポンスボディに載せてアプリケーションに伝えることになりますが、その書式に規定は今までありませんでした。

HTTP API にて、アプリケーションにエラー情報を伝達するための(レスポンスボディに載せられる)標準的な形式が、RFC7807 Problem Details for HTTP APIs で定められています。適用例としては、以下のようになります。

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

{
 "type": "https://example.com/probs/out-of-credit",
 "title": "You do not have enough credit.",
 "detail": "Your current balance is 30, but that costs 50.",
 "instance": "/account/12345/msgs/abc",
 "balance": 30,
 "accounts": ["/account/12345",
              "/account/67890"]
}

仕様自体はそれほど複雑なものではありませんが、RFC 7807 について把握しておくべき章を日本語に訳してみました。

もし API を設計する場面に出くわした際には、この仕様を思い出して、エラー情報の伝達方法を設計されると良いかと思います。


Abstract

この文書は、HTTP APIのための新しいエラーレスポンス書式を定義する必要性を回避するために、HTTPレスポンスにおいて機械が読解可能なエラーの詳細を伝達するための方法として “問題詳細” を定義します。

Introduction

HTTP RFC7230 ステータスコードは、役に立つエラーについて十分な情報を伝えるためには、時として十分ではありません。後ろに人間がいる際には、ウェブブラウザは HTML W3C.REC-html5-20141028 レスポンスボディを使って自然に問題を伝えることができますが、特に”HTTP APIs”と呼ばれる人間ではないコンシューマの場合は、通常そうではありません。

この仕様は、この目的に適した単純な JSON RFC7159 および XML W3C.REC-xml-20081126 ドキュメント書式を定義します。それらは HTTP APIによって再利用されるために策定され、ニーズを反映した明確な”problem types”を識別できます。

従ってAPIクライアントは、(ステータスコードを使った)ハイレベルなエラークラスと、(それらの書式の一つを使った)問題の細かな詳細の両方を伝えることが可能です。

例えば、クライアントの口座が十分な残高を持っていなかったことを示すレスポンスを考えます。レスポンスの一般的なセマンティックスを(クライアントライブラリ、キャッシュ、プロキシのような)HTTPを一般的に扱うソフトウェアに伝える際には、403 Forbidden ステータスコードを使用することが最も適していると考えられます。

しかしながら、なぜ要求が禁止されたのか、適切な口座の残高、または問題の修正方法について、十分な情報をAPIクライアントに提供しません。もしそれらの詳細が機械が読解可能な書式でレスポンスボディに含まれていた場合、例えば、その口座への追加預金の振り込みを促す、といったように、クライアントは適切にそれを扱うことができます。

この仕様は、URI RFC3986 を使った問題タイプの指定を識別(例: “out of credit”)することによって、これを行います。HTTP APIは、それらの制御下で新しいURIを指定することによって、または既存のものを再利用することによって、これを行います。

加えて、問題詳細は、問題の特定の出来事を示すURIのような(”Joeが先週の木曜日に十分な預金を持っていなかった”ことを示す概念の識別子を効果的に与える)、他の情報も含めることができ、それは法務的な目的や支援で役立てることが可能です。

問題詳細のデータモデルは、JSON RFC7159 オブジェクトです。JSONドキュメントとしてフォーマットされた際には、”application/problem+json”メディアタイプが使われます。Appendix A では、等価のXML書式におけるそれらの表現方法が定義されていて、その際には”application/problem+xml”メディアタイプが使われます。

問題詳細は、(当然ながら)HTTPにて問題の詳細を伝える唯一の方法ではないことに注意してください。 レスポンスが依然としてリソースの表現である場合、そのアプリケーションの形式で関連する詳細を記述することで対応する方が望ましい場合がよくあります。 同様に、多くの場合、追加で詳細を伝えることを必要ではなく適切なHTTPステータスコードがあります。

代わりに、この仕様の目的は、既存のHTTPステータスコードのセマンティクスを再定義したくなった際にアプリケーション自身で定義する必要がないように、共通のエラーフォーマットを定義することです。 アプリケーションがエラーを伝えるためにそれを使用しないことを選択したとしても、その設計をレビューすることは、既存の形式でエラーを伝えるときに直面する設計上の決定に役立ちます。

The Problem Details JSON Object

問題詳細の正式なモデルは、JSON RFC7159 オブジェクトです。

JSONドキュメントとしてシリアライズされた際には、その書式は”application/problem+json”メディアタイプで識別されます。

例えば、JSON問題詳細を運ぶHTTPレスポンスは以下になります:

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

{
 "type": "https://example.com/probs/out-of-credit",
 "title": "You do not have enough credit.",
 "detail": "Your current balance is 30, but that costs 50.",
 "instance": "/account/12345/msgs/abc",
 "balance": 30,
 "accounts": ["/account/12345",
              "/account/67890"]
}

ここでは、out-of-credit problem(そのタイプURIで識別される)が”title”において403の理由を示していて、”instance”を使って特定の問題発生箇所の参照を与えていて、”detail”にて発生主体の詳細を与え、そして2つの拡張情報を追加しています。”balance”は口座の残高を伝達し、そして”accounts”は扱うことができる口座へのリンクを与えています。

問題主体の拡張を伝達する能力は、一つ以上の問題の伝達を可能にします。例えば以下です:

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Content-Language: en

{
 "type": "https://example.net/validation-error",
 "title": "Your request parameters didn't validate.",
 "invalid-params": [
    {
     "name": "age",
     "reason": "must be a positive integer"
    },
    {
     "name": "color",
     "reason": "must be 'green', 'red' or 'blue'"}
 ]
}

これは、それぞれの副問題が同じHTTPステータスコードを使用できるだけの十分な類似性を求めることに注意してください。 それらがなければ、207(マルチステータス) RFC4918 コードを使って、複数のステータスメッセージをカプセル化することができます。

Members of a Problem Details Object

問題詳細オブジェクトは、以下のメンバーを持つことができます:

  • “type” (string) - 問題タイプを示すURI参照 RFC3986 です。参照先のデータが得られたとき、人間が読むことができるドキュメントを問題種別から提供することをこの仕様は勧めます(例: HTML W3C.REC-html5-20141028 の使用)。このメンバーがなかったときは、その値は”about:blank”と見なされます。
  • “title” (string) - 問題タイプに関する人間が読むことができる短いサマリーです。ローカライズの目的以外で、問題の発生の度に変更すべきではありません(例: 積極的なコンテンツネゴシエーションの利用、RFC7231, Section 3.4 を参照)。
  • “status” (number) - この問題の発生のための、オリジンサーバによって生成された HTTP ステータスコード(RFC7231, Section 6)。
  • “detail” (string) - この問題の発生を示す人間が読むことができる説明文。
  • “instance” (string) - 特定の問題の発生を示すURI参照。参照先のデータを取得した際には、更なる情報を得られる可能性があります。

コンシューマは、問題タイプのための最も重要な識別子として、”type”文字列を使わなければなりません。”title”文字列はアドバイスであり、URI のセマンティクスを認識せず、それらを検出する能力を持たないユーザのためにのみ含まれるものです(例: オフラインログ分析)。コンシューマは、type URIを自動的に取得すべきではありません。

“status”メンバーは、もし存在している場合は、アドバイスのみであり、コンシューマの利便性のために使われる HTTP ステータスコードを伝達します。ジェネレータは、実際の HTTP レスポンスにおいて同じステータスコードを使用しなければならず、このフォーマットを理解しない一般的な HTTP ソフトウェアが引き続き正しく動作することを保証する必要があります。その利用に関する更なる警告として、Section 5 をご覧ください。

コンシューマは、status メンバーを使用して、ジェネレータによって使用された元のステータスコードが何であったか、変更された場合(例: 仲介者やキャッシュ)、およびメッセージボディが HTTP 情報なしで持続するかどうかを判断できます。 汎用 HTTP ソフトウェアは、引き続き HTTP ステータスコードを使用するでしょう。

“detail” メンバーが存在する場合、デバッグ情報を提供するのではなく、クライアントが問題を修正するのを支援することに焦点を当てるべきです。

コンシューマは、情報のために “detail” メンバーを解析してはなりません。そのような情報を得るには、拡張がより適切であり、エラーを起こしにくい方法です。

“type” と “instance” の両方が、相対URIを受け入れることに注意してください。 これは、RFC3986 Section 5 のように、それらがドキュメントのベース URI に関連して解決されなければならないことを意味します。

Extension Members

問題タイプの定義は、追加メンバーを使って問題詳細オブジェクトを拡張することがあります。

例えば、上記の私たちの “out of credit” 問題は、2つのそのような拡張を定義しています。”balance” と “accounts” は、問題に特化した情報を追加で伝達します。

問題詳細を消費するクライアントは、クライアントが認識しないいかなるそのような拡張を無視しなければなりません。これは、問題タイプについて、将来追加の情報を含めることや進化させることを可能にします。

拡張は、問題タイプによって事実上ネームスペースに入れられるため、新しいメディアタイプを定義せずに新しい「標準」メンバーを定義することはできません。

Defining New Problem Types

HTTP API がエラー状態を示す応答を定義する必要がある場合は、新しい問題タイプを定義することによってそれをすることが適切な場合があります。

そうする前に、それらが良いことなのか、他の仕組みに委ねる方が良いかを理解することが重要です。

問題詳細は、基本となる実装のデバッグツールではありません。 むしろ、HTTP インターフェイス自体について、より詳細な情報を公開するための方法です。 新しい問題タイプの設計者は、Security Considerations(Section 5)、特にエラーメッセージを通じて内部の実装が公開されることによって攻撃方法が公開されてしまうリスクを注意深く検討する必要があります。

同様に、真に一般的な問題、すなわちウェブ上のリソースに潜在的に適用される可能性のある問題は、通常、普通のステータスコードとして表現されます。 例えば、PUT要求に対する403 Forbidden ステータスコードが自明であるため、「書き込みアクセス不許可」問題はおそらく不必要です。

最後に、アプリケーションは、すでに定義されている形式でエラーを伝える、より適切な方法を持っているかもしれません。問題詳細は、既存のドメイン固有の形式を置き換えるのではなく、新しく「失敗」または「エラー」の文書形式を確立する必要性を避けることを目的としています。

つまり、HTTPコンテンツネゴシエーションを使用して既存のHTTP APIに問題詳細のサポートを追加することができます(例: Accept リクエストヘッダーを使用してこのフォーマットのプリファレンスを指定します(RFC7231 Section 5.3.2 を参照)。

新しい問題タイプを定義するには、文書化が必須です:

  1. type URI(典型的には、”http” もしくは “https” スキームを伴う)
  2. それを(短く)適切に説明する title
  3. それを使うことになる HTTP ステータスコード

問題タイプの定義は、適切な状況で Retry-After 応答ヘッダー(RFC7231 Section 7.1.3)の使用を指定しても良いです。

問題タイプの URI は、問題を解決する方法を説明する HTML W3C.REC-html5-20141028 文書にて解決されるべきです。

問題タイプの定義は、問題詳細オブジェクトに追加のメンバーを指定しても良いです。例えば、拡張は、問題を解決するための機械が使用できる別のリソースに対して型付きリンク RFC5988 を使用することがあります。

そのような追加のメンバが定義されているならば、それらの名前は文字(RFC5234 Appendix B.1 に従った ALPHA)で始めるべきであり、ALPHA、DIGIT(RFC5234 Appendix B.1)、”_“(JSON以外の形式で直列化できるように)、そして 3 文字以上であるべきです。

Example

例えば、オンラインショッピングカートを HTTP API で公開している場合、ユーザの残高がないことを示す必要があり(前述の例を参照)、それ故にその購入はできません。

この情報に対応できるアプリケーション固有のフォーマットをすでにお持ちの場合は、おそらくそれを行うのが最善です。 ただし、そうでない場合は、問題詳細の形式(あなたの API が JSONベースであれば JSON、そうでなければ XML 形式)を使用することを検討します。

そうするために、あなたの目的に合った既に定義されたタイプの URI を探すことができるでしょう。それが利用可能であれば、その URI を再利用できます。

利用できない場合は、新しいタイプの URI (あなたの支配下にあるべきであり、時間の経過とともに安定していなければならない)、適切なタイトル、それが適用される HTTP ステータスコード、そしてそれが何を意味してどのように扱うべきかということを作り出して文書化することができます。

要約すれば、インスタンス URI は常に問題の特定の発生を識別します。 一方、問題タイプの適切な記述がすでに他の場所で利用可能である場合、または新しい問題タイプに対して作成できる場合には、タイプ URI を再利用することができます。

Predefined Problem Types

この仕様では、1つの URI を問題タイプとして予約します。

“about:blank” URI RFC6694 が問題タイプとして使用された場合、問題に HTTP ステータスコード以外の追加のセマンティクスがないことを示します。

“about:blank” が使用されている場合、タイトルはクライアントのプリファレンスに合わせてローカライズされているかもしれませんが(”Accept-Language” リクエストヘッダーで表現される)、そのコードに対して推奨された HTTP ステータスフレーズと同じであるべきです(例えば、404 では “Not Found” など)。

“type” メンバの定義(Section 3.1)に従って、 “about:blank” URI はそのメンバのデフォルト値です。 従って、明示的な “type” メンバを持たない問題詳細オブジェクトは、暗黙的にこの URI を使用します。

Security Considerations

新しい問題タイプを定義するときは、含まれる情報を慎重に検証する必要があります。 同様に、問題の実際の発生時に、それは直列化されていますが、詳細も精査されなければなりません。

リスクには、システムを侵害したり、システムにアクセスしたり、システムのユーザーのプライバシーを侵害したりする可能性のある漏洩情報が含まれます。

発生情報へのリンクを提供するジェネレータは、HTTP インタフェースを通じてスタックダンプなどの実装の詳細を掲載することを避けることが推奨されます。これにより、サーバの実装の詳細やデータなどが公開される可能性があります。

“status” メンバは、HTTP ステータスコード自体で利用可能な情報を複製するので、両者の間に不一致が生じる可能性があります。 (例えば)仲介者(例: プロキシまたはキャッシュ)によって転送中の HTTP ステータスコードが変更されたことを示す不一致の可能性があるため、それらの相対的な優先順位は明確ではありません。

そのため、問題タイプや問題の発生者や消費者を定義するものは、汎用ソフトウェア(プロキシ、ロードバランサ、ファイアウォール、ウイルススキャナなど)がこのメンバで伝達されるステータスコードを認識したり尊重したりする可能性は低いことに気づく必要があります。