エラーレスポンスの形を決める
このレッスンでは、エラー応答の本文をどんな形にそろえるかを決め、そのとおりに組み立てられるようになります。
形がばらばらな API は使えません
同じアプリなのに、ある画面では {"error":"not found"} が返り、別の画面では {"message":"見つかりません"} が返り、さらに別のところでは HTML のエラーページが返ってくる。こうした API を相手にすると、クライアントはエラーごとに読み取りのコードを書き分けることになります。エラーの本文は、成功時の本文よりもむしろ形をそろえる価値が高い部分です。成功のときは呼び出し側が何を期待しているか分かっていますが、失敗はいつどこで起きるか分からないため、共通の読み方が必要になるからです。
そろえるべき項目は次の通りです。
| 項目 | 役割 |
|---|---|
| ステータス | HTTP のステータス行と同じ数字。本文だけを見ても状況が分かる |
| エラーコード | プログラムが分岐に使う短い識別子。文言が変わっても壊れない |
| メッセージ | 人が読むための説明。開発者や運用者が原因を掴むためのもの |
エラーコードとメッセージを分けるのが要点です。メッセージは日本語にしたり文面を練り直したりと頻繁に変わりますが、クライアントの分岐がメッセージの文字列に依存していると、文面を直した瞬間に相手のアプリが壊れます。USER_NOT_FOUND のような不変のコードを別に持たせておけば、文面は自由に直せます。
Problem Details という共通の答え
この「エラー本文をどうそろえるか」という問いには、RFC 9457 の Problem Details という標準の答えがあります。以前の RFC 7807 を置き換えたもので、application/problem+json という媒体型で、決まった名前の項目を持つ JSON を返す取り決めです。よく使われる項目は次の通りです。
type— 問題の種類を表す URI。同じ種類のエラーには同じ URI を使うtitle— 種類を短く表した、人が読む見出しstatus— HTTP ステータスコードdetail— この 1 回の発生に固有の説明instance— この 1 回の発生を指す URI
JSON
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your current balance is 30, but that costs 50."
}title は種類ごとに固定、detail は発生ごとに変わる、という役割分担が肝心です。この区別が守られていると、クライアントは type と title で分岐し、detail は画面に出すだけ、という素直な扱いができます。Spring Framework 6 以降には ProblemDetail という型が用意されているので、独自の形を発明する前に、まずこの標準に寄せられないかを検討してください。
本文に載せてはいけないもの
スタックトレース、SQL 文、内部のクラス名、環境変数の値は、エラー本文に載せてはいけません。攻撃者にとっては構成を推測する材料になります。原因の追跡に必要な情報はサーバーのログへ書き、本文には次のレッスンで扱うトレース識別子だけを載せます。利用者から「エラーになりました」と連絡が来たとき、その識別子でログを引ければ十分です。
演習で書くもの
Spring では @RestControllerAdvice のハンドラが返したオブジェクトを、Jackson が JSON へ変換します。ここではその組み立てを自分で書きます。文字列として JSON を作る以上、本文に引用符や円記号が含まれていたときに壊れないよう、エスケープする必要があります。この処理はひな形の補助メソッドに任せ、共通形式を組み立てます。
演習で使うひな形
escape は引用符を \"、バックスラッシュを \\、実際の改行を \n という文字の並びへ変換します。例えば引用符付きの "boom" は \"boom\" になります。code と message をこの補助メソッドに通し、status と合わせて共通形式にしてください。
要件
- status・code・message の順で共通形式を組み立てること
- null の code は UNKNOWN、null の message は空文字にすること
- code と message に escape を適用してから引用符で囲むこと