【実務・中級編】 APIエラーハンドリングの標準化(RFC 7807: Problem Details) – Web APIアーキテクチャ・データ連携実践ガイド

「また500エラーか。中身は……空っぽ? これじゃデバッグのしようがないな」

現場でAPIクライアントの実装をしている時、あるいは深夜のトラブルシューティング中に、こんな独り言を漏らしたことはありませんか?

こんにちは、バックボーンからアプリケーションレイヤーまで、パケットの挙動を追うことに至上の喜びを感じるネットワークプロトコルスペシャリストです。

REST APIの設計において、正常系のレスポンス(200 OKの世界)には心血を注ぐ開発者が多い一方で、異常系(4xxや5xxの世界)の設計は「とりあえずステータスコードを返しておけばいいだろう」と、おざなりにされがちです。しかし、真に「美しいAPI」とは、エラーが起きた時にこそ、クライアントに対して誠実かつ機械的に解釈可能な情報を提供できるものを指します。

今回は、APIエラーハンドリングの救世主、RFC 7807 “Problem Details for HTTP APIs” にスポットを当て、なぜこれが必要なのか、そしてどう実装すべきかを深く掘り下げていきましょう。

—

1. なぜ「オレオレエラー形式」は罪なのか

RESTの原則の一つに「統一インターフェース」があります。しかし、エラーレスポンスに関しては、長年「暗黒時代」が続いていました。

あるAPIは {"error": "invalid_user"} と返し、別のAPIは {"msg": "User not found", "code": 40401} と返す。これでは、クライアント側のエラーハンドリング・ロジックがAPIごとにバラバラになり、共通ライブラリ化することもままなりません。

ネットワーク屋の視点で見れば、これは「プロトコルが定義されていないカオスなセグメント」と同じです。そこで登場したのが、エラー情報の構造を標準化した RFC 7807 です。

—

2. RFC 7807の正体:application/problem+json

RFC 7807は、HTTPレスポンスのボディで「何が起きたのか」を記述するための標準フォーマットを定義しています。最大の特徴は、専用のメディアタイプ application/problem+json(または application/problem+xml)を使用することです。

基本的なデータ構造

標準で定義されている主なフィールドは以下の5つです。

  • type (URI): エラーの種類を特定するURI。理想的には、このURLにアクセスするとエラーの解説ドキュメントが読めるようになっているべきです。
  • title (string): エラーの概要(人間向け)。
  • status (number): HTTPステータスコード(冗長に見えますが、ログ集計時などに重宝します)。
  • detail (string): 今回発生したエラーの具体的な説明。
  • instance (URI): エラーが発生した特定のリソースやリクエストを指し示すURI。

実際のレスポンス例

例えば、残高不足で決済に失敗した場合のレスポンスはこうなります。

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

{
  "type": "https://example.com/probs/out-of-credit",
  "title": "アカウントの残高が不足しています。",
  "status": 403,
  "detail": "現在の残高は 500 円ですが、決済には 800 円必要です。",
  "instance": "/account/12345/msgs/abc",
  "balance": 500,
  "cost": 800
}

ここで注目してほしいのは、balance や cost といった独自の拡張フィールドを自由に追加して良い点です。これにより、クライアントはプログラム的に「あといくら足りないのか」を判断し、UIに反映させることが可能になります。

—

3. 【実践】サーバーサイドでの実装例(Python/FastAPI風)

現代的なフレームワークを使って、RFC 7807に準拠したエラーを返してみましょう。ここではPythonのFastAPIを例に挙げますが、考え方はどの言語でも同じです。

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse

app = FastAPI()

# カスタム例外の定義
class ProblemDetailsException(Exception):
    def __init__(self, status_code: int, type: str, title: str, detail: str, instance: str = None, **kwargs):
        self.status_code = status_code
        self.payload = {
            "type": type,
            "title": title,
            "status": status_code,
            "detail": detail,
            "instance": instance,
            **kwargs
        }

# 例外ハンドラー:ここで Content-Type を強制する
@app.exception_handler(ProblemDetailsException)
async def problem_details_handler(request: Request, exc: ProblemDetailsException):
    return JSONResponse(
        status_code=exc.status_code,
        content=exc.payload,
        media_type="application/problem+json"  # RFC 7807の肝
    )

@app.get("/items/{item_id}")
async def read_item(item_id: str):
    if item_id == "forbidden":
        raise ProblemDetailsException(
            status_code=403,
            type="https://api.example.com/errors/no-permission",
            title="アクセス権限がありません",
            detail=f"アイテム {item_id} を閲覧する権限が付与されていません。",
            instance=f"/items/{item_id}"
        )
    return {"item_id": item_id}

—

4. 【実践】クライアントサイドでのハンドリング(Fetch API)

クライアント側では、Content-Type を見て処理を分岐させるのがプロの仕草です。

async function fetchItem(itemId) {
  const response = await fetch(`/items/${itemId}`);

  if (!response.ok) {
    // Content-Typeが application/problem+json かどうかを確認
    const contentType = response.headers.get("Content-Type");
    
    if (contentType && contentType.includes("application/problem+json")) {
      const errorDetail = await response.json();
      console.error(`Error Type: ${errorDetail.type}`);
      console.error(`Message: ${errorDetail.title} - ${errorDetail.detail}`);
      
      // typeに応じて特定のリカバリ処理を行う例
      if (errorDetail.type === "https://api.example.com/errors/no-permission") {
          // 権限不足のモーダルを表示するなど
      }
    } else {
      // 従来の汎用的なエラー処理
      console.error("予期しないエラーが発生しました。");
    }
    return;
  }

  const data = await response.json();
  console.log("Success:", data);
}

—

5. 現場のデバッグで役立つ curl のテクニック

インフラエンジニアなら、トラブル時にまず叩くのは curl ですよね。RFC 7807を導入しているAPIを調査する際は、必ず -i オプションでヘッダーを確認してください。

# -i オプションでレスポンスヘッダーを表示
curl -i https://api.example.com/items/forbidden

# 期待される出力(一部抜粋)
# HTTP/1.1 403 Forbidden
# Content-Type: application/problem+json  <-- ここをチェック!
# 
# {
#   "type": "https://api.example.com/errors/no-permission",
#   ...
# }

もし、Content-Type が application/json のままだったり、ボディが空だったりしたら、それは設計者に「RFC 7807という良い標準があるよ」と教えてあげるチャンスです。

—

6. シニアエンジニアからのアドバイス:設計の勘所

RFC 7807を導入するにあたって、私が現場で大切にしているポイントを3つ伝えます。

1. type を疎かにしない:
単なる about:blank にせず、必ず自社のドキュメントサイトのURLを指すようにしましょう。開発者がそのURLをクリックして、解決策(例えば「APIキーの有効期限が切れています」など)に辿り着けるようにするのが、究極のホスピタリティです。
2. セキュリティとのバランス:
detail にスタックトレースや内部サーバーのIPアドレス、DBのクエリなどを出力してはいけません。それはセキュリティ脆弱性になります。詳細はログに吐き出し、instance フィールドに「ログ追跡用リクエストID」を載せるのがスマートです。
3. 既存APIとの互換性:
既存のAPIに導入する場合、クライアントが Accept: application/problem+json を送ってきた場合のみこの形式で返す、というネゴシエーションの実装も検討の余地があります。

まとめ

RFC 7807は、単なるフォーマットの定義ではありません。それは「API提供者と利用者の間の対話を標準化する」という、疎結合なシステム構築における極めて重要な規約です。

エラーは必ず起きます。その時に、パケットの中にどれだけ価値のある情報を詰め込めるか。そこに、エンジニアとしての品格が表れると私は信じています。

あなたのAPIが、エラーの向こう側にいる開発者を笑顔にすることを願っています。それでは、良きプロトコルライフを!

コメント

タイトルとURLをコピーしました