17
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

APIエラーレスポンスを標準化する - RFC9457移行で得られる5つのメリット

Last updated at Posted at 2025-12-11

こちらはHubble Advent Calendar 2025の12日目の記事です。

はじめに

株式会社Hubbleでバックエンドエンジニアを担当している @horin0211 です。

みなさんは API を開発していて、こんな経験はありませんか?

「このエンドポイントのエラー形式、どうなってたっけ?」
「フロントエンドから、エラーメッセージの取得方法がAPIごとに違うって指摘された...」
「新しいメンバーが、エラーハンドリングのコードを見て混乱してる」

APIのエラーレスポンス形式が統一されておらず、プロジェクトやエンドポイントごとに構造がばらつくことは少なくありません

// あるエンドポイントのエラー
{
  "error": "User not found",
  "code": 404
}

// 別のエンドポイントのエラー
{
  "message": "Invalid input",
  "error_code": "VALIDATION_ERROR",
  "status": 400
}

// さらに別のエンドポイント...
{
  "success": false,
  "errors": ["Email is required"],
  "statusCode": 422
}

このような状況は技術的負債を増やし、開発効率を下げ、最終的にはユーザー体験にも悪影響を及ぼします。

そこで有効なのが、IETF(Internet Engineering Task Force)が策定した RFC9457 です。これは HTTP ベースの API におけるエラーレスポンスの標準形式を定めた仕様で、RFC9457 に移行することで開発現場に大きなメリットをもたらします。

実際に、Hubble の開発チームでは RFC9457 への移行を検討しています。

本記事は、その第一歩として、RFC9457 がどのようなメリットをもたらすのかを整理するために執筆しました。

RFC9457とは

RFC9457(Problem Details for HTTP APIs)は、2023年7月に公開されたIETF標準で、HTTP ベースの API におけるエラー情報の表現方法を定義しています。

基本構造

RFC9457 では、以下の5つの標準フィールドを定義しています:

{
  "type": "https://example.com/problems/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "The 'email' field is required but was not provided.",
  "instance": "/users/create/req-12345"
}

各フィールドの役割:

  • type: エラーの種類を示すURI(問題の型を一意に識別)
  • title: エラーの簡潔な説明(人間が読める形式)
  • status: HTTPステータスコード
  • detail: エラーの詳細な説明(特定の発生状況に応じた情報)
  • instance: エラーが発生した特定のリクエストを識別するURI

すべてのフィールドはオプショナルです。type を省略した場合は about:blank が既定値となります。実務では title と status を基本に、必要に応じて detail や instance、拡張フィールドを付与します。

Content-Type は application/problem+json

RFC9457 準拠のレスポンスは、専用のメディアタイプ application/problem+json を使用します。これにより、クライアント側で標準化されたエラーレスポンスであることを即座に認識できます。

それでは、この標準形式に移行することで、どのような具体的なメリットが得られるのでしょうか?

メリット1: 開発者体験の向上

フロントエンド開発者の負担軽減

RFC9457 に準拠することで、フロントエンド開発者は一度エラーハンドリングを実装すれば、すべてのAPIで使い回せるようになります。

従来の課題:

// エンドポイントAのエラーハンドリング
if (responseA.error) {
  showError(responseA.error);
}

// エンドポイントBのエラーハンドリング
if (responseB.message) {
  showError(responseB.message);
}

// エンドポイントCのエラーハンドリング
if (!responseC.success && responseC.errors) {
  showError(responseC.errors[0]);
}

RFC9457準拠の場合:

// すべてのAPIで共通のエラーハンドリング
async function handleApiError(response: Response) {
  if (!response.ok) {
    const problem = await response.json();

    // 標準フィールドに確実にアクセスできる
    showError(problem.title || 'An error occurred');

    // typeに基づいた詳細なハンドリングも可能
    if (problem.type === 'https://api.example.com/problems/validation-error') {
      handleValidationError(problem);
    }
  }
}

新規参加メンバーのオンボーディング時間短縮

「このプロジェクトのエラーレスポンス形式は...」という説明が不要になります。RFC9457 という業界標準を知っていれば、すぐに開発に取り掛かれます。

タイプセーフな実装が容易

TypeScript などの型システムを持つ言語では、標準化された型定義を活用できます:

interface ProblemDetails {
  type?: string;
  title?: string;
  status?: number;
  detail?: string;
  instance?: string;
  [key: string]: any; // 拡張フィールド用
}

// すべてのAPIで同じ型を使える
const handleError = (problem: ProblemDetails) => {
  // 型安全なエラーハンドリング
};

メリット2: デバッグ効率の向上

type によるエラーの一意識別

エラーの種類を URI 形式で表現することで、エラーを一意に識別できます。

{
  "type": "https://api.example.com/problems/insufficient-balance",
  "title": "Insufficient Balance",
  "status": 400,
  "detail": "Your account balance is ¥500, but the transaction requires ¥1,000.",
  "balance": 500,
  "required": 1000
}

この type を見るだけで:

  • どの種類のエラーか即座に判別できる
  • ログ検索が容易になる
  • エラー統計の集計が正確に行える

instance による追跡の容易さ

instance フィールドに一意のリクエストIDを含めることで、特定のエラー発生箇所を追跡しやすくなります。

{
  "type": "https://api.example.com/problems/database-error",
  "title": "Database Error",
  "status": 500,
  "detail": "Failed to connect to database.",
  "instance": "/api/users/12345/orders/req-abc-123"
}

運用チームはこの instance をログシステムで検索し、リクエスト全体のトレースを即座に取得できます。

構造化されたログとの親和性

RFC9457 の構造化された形式は、JSON形式のログシステム(CloudWatch、Datadog、Elasticsearchなど)と相性が抜群です。

// アプリケーションログ
{
  "timestamp": "2024-12-07T10:30:00Z",
  "level": "error",
  "problem": {
    "type": "https://api.example.com/problems/validation-error",
    "title": "Validation Error",
    "status": 400,
    "instance": "/api/users/create/req-xyz-789",
    "validation_errors": {
      "email": "Invalid email format"
    }
  },
  "user_id": 12345,
  "ip": "192.168.1.1"
}

type や instance でフィルタリングすることで、特定のエラーパターンを瞬時に分析できます。

メリット3: ドキュメント化の負担軽減

「基本構造」の説明が不要

従来のAPI仕様書では、エラーレスポンスの形式を毎回詳しく説明する必要がありました。

従来のドキュメント:

### エラーレスポンス形式

エラーが発生した場合、以下の形式でレスポンスを返します:

- error_code (string): エラーコード
- message (string): エラーメッセージ
- details (object, optional): 詳細情報
- timestamp (string): エラー発生時刻

例:
{
  "error_code": "VALIDATION_ERROR",
  "message": "Invalid input",
  ...
}

RFC9457準拠の場合:

### エラーレスポンス

本APIはRFC9457 (Problem Details for HTTP APIs)に準拠しています。
詳細は https://www.rfc-editor.org/rfc/rfc9457.html を参照してください。

#### エラータイプ一覧

| type | 説明 |
|------|------|
| https://api.example.com/problems/validation-error | 入力値が不正 |
| https://api.example.com/problems/not-found | リソースが見つからない |
...

基本構造の説明が不要になり、エラーの種類とその意味の説明に注力できます。

type URIにドキュメントをリンク

type フィールドの URI を実際のドキュメントページにすることで、エラーの詳細情報へ直接アクセスできます。

{
  "type": "https://api.example.com/docs/problems/validation-error",
  "title": "Validation Error",
  ...
}

この URL にアクセスすれば、エラーの原因や対処法、関連情報を確認できます。開発者は、エラーに遭遇したその瞬間から必要な情報にすぐにたどり着けます。

OpenAPI仕様との統合

OpenAPI 3.0以降では、RFC9457形式のエラーレスポンスを簡潔に定義できます:

components:
  schemas:
    ProblemDetails:
      type: object
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
          format: uri

  responses:
    ValidationError:
      description: Validation Error
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/ProblemDetails'

一度定義すれば、すべてのエンドポイントで参照できます。

メリット4: クライアント側の柔軟な対応

機械可読な構造による自動化

RFC9457の構造化された形式により、エラーハンドリングの自動化が容易になります。

// typeに基づいた自動リトライ
async function apiCall(url: string, options: RequestInit) {
  const response = await fetch(url, options);

  if (!response.ok) {
    const problem: ProblemDetails = await response.json();

    // 特定のエラータイプの場合は自動リトライ
    if (problem.type === 'https://api.example.com/problems/rate-limit') {
      const retryAfter = problem.retryAfter || 60;
      await sleep(retryAfter * 1000);
      return apiCall(url, options); // リトライ
    }

    throw problem;
  }

  return response.json();
}

多言語対応の容易さ

titledetailを分離することで、多言語対応が簡潔になります。

// クライアント側で言語に応じたメッセージを表示
const errorMessages = {
  'ja': {
    'https://api.example.com/problems/validation-error': 'バリデーションエラー',
    'https://api.example.com/problems/not-found': 'リソースが見つかりません'
  },
  'en': {
    'https://api.example.com/problems/validation-error': 'Validation Error',
    'https://api.example.com/problems/not-found': 'Resource Not Found'
  }
};

function getLocalizedMessage(problem: ProblemDetails, lang: string) {
  return errorMessages[lang][problem.type] || problem.title;
}

サーバー側で多言語対応する場合でも、title(汎用的な説明)とdetail(具体的な状況)を分けることで、翻訳の粒度を適切に保てます。

拡張フィールドによる柔軟な情報提供

RFC9457 は標準フィールドに加えて、独自フィールドの追加を許可しています。

{
  "type": "https://api.example.com/problems/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "One or more fields failed validation.",
  "instance": "/api/users/create/req-abc-123",
  "validation_errors": {
    "email": {
      "message": "Invalid email format",
      "provided_value": "invalid-email",
      "expected_format": "[user@example.com](mailto:user@example.com)"
    },
    "age": {
      "message": "Must be at least 18",
      "provided_value": 15,
      "minimum": 18
    }
  }
}

クライアント側は標準フィールドで基本的なエラーハンドリングを行い、拡張フィールドで詳細な処理を実装できます。

メリット5: 業界標準への準拠とエコシステムの恩恵

サードパーティツールとの親和性

RFC9457に準拠することで、様々なツールやライブラリとの統合が容易になります。

モニタリングツール:

  • Datadog、New Relic、Sentry などは、RFC9457 形式のエラーを自動認識し、適切にグループ化・分析できます
  • type ベースでエラーをカテゴライズし、アラート設定が簡単に

APIテストツール:

  • Postman、Insomnia、Paw などのツールは、application/problem+json を認識し、専用のUIでエラー情報を表示できます

他のAPIとの統合が容易

マイクロサービスアーキテクチャでは、複数のサービスが連携します。すべてのサービスがRFC9457 に準拠していれば、エラーの伝播と処理が統一されます。

// サービスAからサービスBを呼び出す
async function callServiceB(data: any) {
  try {
    const response = await fetch('https://service-b/api/endpoint', {
      method: 'POST',
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      const problem = await response.json();

      // RFC9457形式なので、そのまま上流に伝播できる
      throw problem;
    }

    return response.json();
  } catch (error) {
    // エラーをRFC9457形式で上流に返す
    throw error;
  }
}

チーム間・企業間での共通言語

RFC9457 は国際標準です。この標準を採用することで:

  • 外部API との連携時に、エラーハンドリングの仕様調整が不要
  • チーム間のコミュニケーションで「RFC9457 の type」と言えば伝わる
  • 転職してきたエンジニアもすぐに理解できる

技術的な意思決定の根拠として「IETF 標準に準拠」と説明できることも大きなメリットです。

RFC9457への移行でどう変わるのか?

ここからは、Hubble API の具体的なエラーパターンを題材に、RFC9457 準拠へ移行した際の違いとその効果を、実際のレスポンス例で比較していきましょう。

現在のHubbleのエラーレスポンス形式

Hubble APIでは現在、以下の形式でエラーレスポンスを返しています:

def render_error(code, description)
  error = { code:, description: }
  render json: { success: false, error: }, status: code
end

ケース1: バリデーションエラー(入力値の不正)

現在のHubbleの形式:

{
  "success": false,
  "error": {
    "code": "bad_request",
    "description": "The 'email' field is required but was not provided."
  }
}

課題点:

  • どのフィールドにエラーがあるかが文字列に埋もれている
  • エラーの種類を識別するには description を解析する必要がある
  • リクエストの追跡情報がない

RFC9457準拠の形式:

{
  "type": "https://api.hubble.co.jp/problems/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "One or more fields failed validation.",
  "instance": "/api/v1/users/create/req-abc-123",
  "validation_errors": {
    "email": {
      "message": "Email is required",
      "field": "email"
    }
  }
}

改善点:

  • type でエラーの種類を一意に識別
  • バリデーションエラーの詳細が構造化されている
  • instance でリクエストを追跡可能(DataDogでの検索が容易)

ケース2: 権限エラー(ドキュメントへのアクセス拒否)

Hubble では多くの権限関連のカスタムステータスコードがあります(464, 465, 466, 497など)。

現在のHubbleの形式(ステータス464):

def render_464(exception = nil)
  Rails.logger.error(exception) if exception.is_a?(StandardError)
  render_error(exception.code, exception&.message.presence || "No permission for document")
end
{
  "success": false,
  "error": {
    "code": 464,
    "description": "No permission for document"
  }
}

課題点:

  • カスタムステータスコード(464)の意味がパッと見で分からない
  • どのドキュメントに対する権限がないのか不明
  • エラーの種類を判別するのにステータスコードの一覧を覚える必要がある

RFC9457準拠の形式:

{
  "type": "https://api.hubble.co.jp/problems/no-permission-for-document",
  "title": "No Permission for Document",
  "status": 403,
  "detail": "You do not have permission to access document ID 12345.",
  "instance": "/api/v1/documents/12345/req-def-456",
  "document_id": 12345,
  "required_permission": "read",
  "user_permissions": ["comment"]
}

改善点:

  • 標準的なHTTPステータス(403)を使用
  • type で詳細なエラー種別を識別(カスタムコードが不要に)
  • どのドキュメントか、どの権限が必要かが明確
  • DataDog で type や document_id でフィルタリング可能

ケース3: レート制限エラー

Hubble の API にはレート制限の仕組みがあります。

現在のHubbleの形式:

def render_429(exception = nil)
  Rails.logger.error(exception)
  render_error(:too_many_requests, exception&.message.presence || "Rate Limit Exceeded")
end
{
  "success": false,
  "error": {
    "code": "too_many_requests",
    "description": "Rate Limit Exceeded"
  }
}

課題点:

  • いつリトライできるかの情報がない
  • どのタイプのレート制限(ユーザー単位/組織単位/API単位)かが不明
  • 現在のカウント数や上限値が分からない

RFC9457準拠の形式:

{
  "type": "https://api.hubble.co.jp/problems/rate-limit-exceeded",
  "title": "Rate Limit Exceeded",
  "status": 429,
  "detail": "User has exceeded the API rate limit.",
  "instance": "/api/v1/projects/search/req-xyz-789",
  "limit_type": "user_api",
  "limit": 1000,
  "current_count": 1001,
  "retry_after": 3600,
  "reset_at": "2024-12-07T11:00:00Z"
}

改善点:

  • リトライ可能時刻が明確(自動リトライ実装が容易)
  • レート制限の種類と詳細情報が構造化
  • DataDog でレート制限の分析が容易(どの API がよく制限されるか等)

ケース4: バルクインポートのエラー(CSV重複ヘッダー)

Hubble には複数のバルクインポート関連のカスタムエラーがあります。

現在のHubbleの形式(ステータス493):

def render_493(exception = nil)
  Rails.logger.error(exception) if exception.is_a?(StandardError)
  render_error(493, exception&.message.presence || "CSV headers duplicated")
end
{
  "success": false,
  "error": {
    "code": 493,
    "description": "CSV headers duplicated"
  }
}

課題点:

  • どのヘッダーが重複しているか不明
  • 修正方法が分からない
  • カスタムステータスコード493の意味がドキュメントを見ないと分からない

RFC9457準拠の形式:

{
  "type": "https://api.hubble.co.jp/problems/csv-duplicated-headers",
  "title": "CSV Headers Duplicated",
  "status": 400,
  "detail": "The CSV file contains duplicated header names.",
  "instance": "/api/v1/bulk-import/req-ghi-012",
  "duplicated_headers": ["email", "name"],
  "line_numbers": [1],
  "suggestion": "Ensure each column header appears only once in the CSV file."
}

改善点:

  • どのヘッダーが重複しているか明確
  • 修正のヒントを提供
  • 標準的なステータスコード(400)を使用

ケース5: データベースロックタイムアウト

現在のHubbleの形式:

def render_423(exception = nil)
  Rails.logger.error(exception) if exception.is_a?(StandardError)
  render_error(:locked, "Lock Timeout")
end
{
  "success": false,
  "error": {
    "code": "locked",
    "description": "Lock Timeout"
  }
}

課題点:

  • どのリソースがロックされているか不明
  • リトライすべきかどうかの判断材料がない

RFC9457準拠の形式:

{
  "type": "https://api.hubble.co.jp/problems/resource-locked",
  "title": "Resource Locked",
  "status": 423,
  "detail": "The requested resource is currently locked by another transaction.",
  "instance": "/api/v1/projects/12345/update/req-jkl-345",
  "resource_type": "Project",
  "resource_id": 12345,
  "retry_recommended": true,
  "retry_after": 5
}

改善点:

  • どのリソースがロックされているか明確
  • リトライ推奨とその時間が分かる
  • 自動リトライロジックを実装しやすい

まとめ

本記事では、RFC9457 へ移行する意義と効果についてまとめました。

RFC9457 は公開以降、採用が加速しています。今のうちに標準化に着手すれば、将来の技術的負債を抑えながら開発効率とユーザー体験を着実に引き上げられます。

API エラーレスポンスの標準化は、見た目の統一にとどまりません。保守性や運用品質、開発速度を同時に高める実践的な投資です。まだ導入していない方は、この機会に採用を検討してみてください!

ここまで読んで頂きまして、ありがとうございました!
明日 12/13(土) は @okakee さんです!

参考リンク

17
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?