問題の詳細 (RFC 9457): APIエラー処理を実際に使ってみる

おかえりなさい!Problem Details標準を使って、APIエラー処理の世界に飛び込みましょう。 パートIでは、一貫性のないエラー報告から生じるいくつかの課題について説明しました。これらの課題は、APIの生産が増加するにつれてデジタルエコシステムが拡大するにつれて増幅されます。実行可能なエラーメッセージの提供の重要性から、カスタムエラーメソッドやセキュリティ脆弱性の落とし穴まで、RFC 9457のProblem DetailsがAPIエラー応答を標準化するための堅牢なフレームワークをどのように提供するかを見てきました。

RFC 7807からRFC 9457への進化について強調し、APIエラーの明確化における重要な進歩を含め、エラー処理を開発者の悪夢から合理化された、情報豊富な、実行可能なプロセスへと変えました。この標準は、開発者にとってエラー処理をより直感的にするだけでなく、デジタルプラットフォーム全体のセキュリティと一貫性を向上させます。

さて、標準の利点とフレームワークを十分に理解したところで、実践的な実装に移行しましょう。このセグメントでは、実際の例とリソースを使用して、システムが可能な限り最善の方法で「悪い知らせ」を配信できるように、API内でProblem Detailsを活用する方法を案内します。

Problem Detailsの開始

チームまたは組織に新しい標準を導入する際、開始が最も難しいステップとなることがよくあります。まず、RFC 9457 [1]に慣れてください。

ゼロから始めない

よくある落とし穴を避け、Problem Detailsの採用を加速するために、以下のリソースと成果物を活用してください。これにより、採用をブートストラップし、チームと一貫したエラー処理を提供するための道を切り開くことができます。

レジストリ

シリーズのパートIで述べたように、新しいRFC 9457は、一般的な問題タイプのURIのレジストリの概念を導入しています。IANA [2]でホストされている公式レジストリとSmartBear Problems Registry [3]は、APIに関連する問題タイプを決定する際に貴重なリソースとして機能します。

  • IANAレジストリ: 公式レジストリには、すぐに使用できる、または独自の問題タイプを定義する際の参照として使用できる標準化された問題タイプURIが保持されています。
  • SmartBear Problems Registry: このレジストリは、SmartBearチームがキュレーションした、さまざまなAPIシナリオに固有の一般的な問題タイプのカタログを提供しています。将来的には、一部がIANAレジストリに移行する可能性があります。

オブジェクト、スキーマ、拡張性

標準の利点は、エラー詳細の形状に関連する多くの議論を回避できることです。RFCは、以下の非規範的なHTTP Problem Details用のJSONスキーマを提供しており、エラーの基本形状を保証します。

上記のJSONスキーマでカバーされる以上の情報を提供する必要がある状況では、組み込みの拡張性が強力なメカニズムであり、チームのニーズに合わせて標準を調整できるので安心してください。

拡張性には独自の課題があります。したがって、拡張ポイントを明確に定義し、Problem Detailsを利用するクライアントに、認識しない拡張を無視しなければならないことを伝えるのが良い習慣です。実装者(および応答を消費する者)にとってプロセスを予測可能にするために、拡張を含むJSONスキーマを作成することも推奨します。

以下は、SmartBearの新しいAPI内で問題の詳細に使用している`errors`および`code`拡張を含むJSONスキーマ [5]です。

{
    "$schema": "https://json-schema.dokyumento.jp/draft/2019-09/schema",
    "type": "object",
    "properties": {
      "type": {
        "type": "string",
        "description": "A URI reference that identifies the problem type.",
        "format": "uri",
        "maxLength": 1024
      },
      "status": {
        "type": "integer",
        "description": "The HTTP status code generated by the origin server for this occurrence of the problem.",
        "format": "int32",
        "minimum": 100,
        "maximum": 599
      },
      "title": {
        "type": "string",
        "description": "A short, human-readable summary of the problem type. It should not change from occurrence to occurrence of the problem, except for purposes of localization.",
       "maxLength": 1024
      },
     "detail": {
        "type": "string",
        "description": "A human-readable explanation specific to this occurrence of the problem.",
        "maxLength": 4096
      },
      "instance": {
        "type": "string",
        "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.",
        "maxLength": 1024
      },
      "code": {
        "type": "string",
        "description": "An API specific error code aiding the provider team understand the error based on their own potential taxonomy or registry.",
        "maxLength": 50
      },
      "errors": {
        "type": "array",
        "description": "An array of error details to accompany a problem details response.",
        "maxItems": 1000,
        "items": {
          "type": "object",
          "description": "An object to provide explicit details on a problem towards an API consumer.",
          "properties": {
            "detail": {
              "type": "string",
              "description": "A granular description on the specific error related to a body property, query parameter, path parameters, and/or header.",
              "maxLength": 4096
            },
            "pointer": {
              "type": "string",
              "description": "A JSON Pointer to a specific request body property that is the source of error.",
              "maxLength": 1024
            },
            "parameter": {
              "type": "string",
              "description": "The name of the query or path parameter that is the source of error.",
              "maxLength": 1024
            },
            "header": {
              "type": "string",
              "description": "The name of the header that is the source of error.",
              "maxLength": 1024
            },
            "code": {
              "type": "string",
              "description": "A string containing additional provider specific codes to identify the error context.",
              "maxLength": 50
            }
          },
          "required": [
            "detail"
          ]
        }
      }
    },
    "required": [
      "detail"
    ]
  }

これにより、パラメーターまたはリクエスト本文に関連するエラーの発生を記述する強力かつ詳細な機能が提供されます。

上記のJSONスキーマに基づいたいくつかの例を見てみましょう

  1. 欠落しているリクエストパラメーター(クエリパラメーターなど)の問題については、`errors`拡張を利用して、`details`および`parameter`プロパティを介して欠落しているパラメーターに関する明示的な情報を提供できます。
{
    "type": "https://problems-registry.smartbear.com/missing-request-parameter",
    "status": 400,
    "title": "Missing request parameter",
    "detail": "The request is missing an expected query or path parameter.",
    "code": "400-03",
    "errors": [
      {
        "detail": "The query parameter {name} is required.",
        "parameter": "name"
      }
    ]
  }
  1. 不正な形式のリクエストボディプロパティの問題については、`errors`拡張を利用して、問題に関する明示的な情報と、`details`および`pointer`(プロパティの場所へのJSONポインターを指定)プロパティを介したプロパティの場所を提供できます。
{
    "type": "https://problems-registry.smartbear.com/invalid-body-property-format",
    "status": 400,
    "title": "Invalid Body Property Format",
    "detail": "The request body contains a malformed property.",
    "code": "400-04",
    "errors": [
      {
        "detail": "Must be a positive integer",
        "pointer": "/quantity"
      }
    ]
  }
  1. 複数のエラーが見つかり、過剰なチャット的なエンゲージメントを強制するのではなく、すべての違反をクライアントに返したい場合は、`errors`配列拡張を利用して、関連する問題タイプに適用されるすべてのエラーの詳細を含めることができます。
{
    "type": "https://problems-registry.smartbear.com/business-rule-violation",
    "status": 422,
    "title": "Business Rule Violation",
    "detail": "The request body is invalid and not meeting business rules.",
    "code": "422-01",
    "errors": [
        {
        "detail": "Maximum quantity allowed in 999",
        "pointer": "/quantity"
        },
        {
        "detail": "We do not offer `next-day` delivery to non-EU addresses",
        "pointer": "/shippingAddress/country"
        },
        {
        "detail": "We do not offer `next-day` delivery to non-EU addresses",
        "pointer": "/shippingOption"
        }
    ]
}

OpenAPIで既製のドメインで加速する

次のAPIプロジェクトに問題の詳細をさらに簡単に導入するために、上記の資産をすぐに使用できるSwaggerHubドメイン [4]にまとめました。これは、OpenAPI記述のさまざまな部分から参照できます。

ドメインは以下で構成されています

  • スキーマ: HTTP Problem Detailsの完全版および拡張版(別名「オピニオン付き」)スキーマ

  • 例: サポートされている問題タイプに対する代表的な応答例の豊富なリスト

  • 応答: サポートされている問題タイプに対するOpenAPI互換応答の参照可能なリスト

以下のセクションでは、OpenAPI記述内で無料の公開ドメインを活用する例を説明します。

OpenAPIで問題の詳細を使用する

OpenAPIの記述でProblem Detailsを活用することは、思っているよりも簡単です。さらに、上記の成果物の一部を利用することで、さらに簡単になります。Bookstore APIの簡単なOpenAPI記述を作成して、API設計におけるエラー応答を改善できることを示しましょう。

OpenAPI記述の最初のパスでは、基本オブジェクト(情報、タグ、サーバー)、書籍を取得し注文を出すための2つのリソース、および書籍と注文リソースに関連するスキーマを設定します。

openapi: 3.0.3
info:
  title: Bookstore APIversion: 0.0.1description: |
    The **Books API** - allows searching of books from the book catalog as well as retrieving the specific details on a selected book. Once you find the book you are looking for, you can make an order.termsOfService: https://swagger.dokyumento.jp/terms/contact: 
    name: DevRel at SmartBear
    email: [email protected]license: 
    name: Apache 2.0
    url: https://apache.dokyumento.jp/licenses/LICENSE-2.0.html

tags:
  - name: Bookstore
    description: APIs for our fictional bookstore

servers:
  # Added by API Auto Mocking Plugin
  - description: SwaggerHub API Auto Mocking
    url: https://virtserver.swaggerhub.com/frank-kilcommins/Bookstore-API/1.0.0paths:
  /books:
    get:
      summary: Get a list of books based on the provided criteria
      description: |
        This API method supports searching the book catalog based on book title or author name
      operationId: getBooks
      tags: 
        - Bookstore
      parameters: 
      - name: title
        description: The title (or partial title) of a book
        in: query
        required: false
        schema:
          type: string
          maxLength: 200
          format: string
      - name: author
        description: The author’s name (or partial author name)
        in: query
        required: false
        schema:
          type: string
          maxLength: 150
          format: string
      - name: limit
        description: The maximum number of books to return
        in: query
        required: false
        schema:
          type: integer
          format: int64
          minimum: 1
          maximum: 1000
          default: 10
      responses:
        '200':
          $ref: '#/components/responses/books'
        '400':
          description: 400 Bad Request
        '401':
          description: 401 Unauthorized
        '500':
          description: 500 Internal Server Error
          content:
            application/json:
              schema:
                type: object
                properties:
                  code:
                    type: integer
                    format: int32
                    example: 500
                  message:
                    type: string
                    example: "Internal Server Error"
                  details:
                    type: string
                    example: "An unexpected error occurred"/orders:
    post:
      summary: Place book order
      description: |
        This API method allows placing an order for one or more books
      operationId: createOrder
      tags:
      - Bookstore
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Order'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderDetails'
        '401':
          description: 401 Unauthorized
        '422':
          description: Validation Error
        '500':
          description: Internal Server Error
components:
  schemas:
    BookOrder:
      type: object
      properties:
        bookId:
          type: string
          description: The book identifier
          format: uuid
          maxLength: 36
          example: 87da4501-4b52-4ea2-a2be-7dda8650f7eb
        quantity:
          type: integer
          format: int64
          minimum: 1
          maximum: 10000
    Order:
      properties:
        books:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/BookOrder'
        deliveryAddress:
          type: string
          minLength: 10
          maxLength: 500
      type: object
      required:
        - books
        - deliveryAddress
      additionalProperties: false
    OrderDetails:
      properties:
        books:
          type: array
          description: The books that are part of the order
          maxItems: 1000
          items:
            $ref: '#/components/schemas/BookOrder'
        deliveryAddress:
          type: string
          description: The address to deliver the order to
          maxLength: 1000
        id:
          type: string
          description: The order identifier
          format: uuid
          maxLength: 36
          example: 87da4501-4b52-4ea2-a2be-7dda8650f7eb
        createdAt:
          type: string
          description: When the order was created
          format: date-time
          pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[012][0-9]:[0-5][0-9]:[0-5][0-9]Z$'
          maxLength: 250
        updatedAt:
          type: string
          description: When the order was updated
          format: date-time
          pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}T[012][0-9]:[0-5][0-9]:[0-5][0-9]Z$'
          maxLength: 250
        status:
          $ref: '#/components/schemas/OrderStatusEnum'
      type: object
      required:
        - books
        - deliveryAddress
        - id
        - createdAt
        - updatedAt
    OrderStatusEnum:
      type: string
      enum:
        - placed
        - paid
        - delivered

    Book:
      description: The schema object for a Book
      type: object
      additionalProperties: false
      properties:
        id:
          description: the identifier for a book
          type: string
          format: uuid
          maxLength: 36
          example: 87da4501-4b52-4ea2-a2be-7dda8650f7eb
        title:
          type: string
          description: The book title
          maxLength: 1000
          example: "Designing APIs with Swagger and OpenAPI"
        authors:
          type: array
          description: A list of book authors
          maxItems: 1000
          items:
            type: string
            description: A string containing an author's name
            maxLength: 250
            minItems: 1
            maxItems: 1000
            example: "[Joshua S. Ponelat, Lukas L. Rosenstock]"
        published:
          type: string
          format: date
          pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
          maxLength: 250
          example: "2022-05-01"responses:
    books:
      description: List of books
      content:
        application/json:
          schema:
            type: array
            maxItems: 1000
            items:
              $ref: '#/components/schemas/Book'
  
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: api_keysecurity:
  - ApiKeyAuth: []

この簡単なOpenAPI記述により、複数のエラー応答をカバーしているため堅牢だと考えられるインタラクティブなAPIドキュメントが生成されます。しかし、APIで発生する可能性のあるエラーを浮上させるのに役立つ重要な要素が不足しています。

一貫性のないエラーをどうすれば検出できますか?

API内でHTTP Problem Details標準を適用し忘れないように、以下のSpectralルールをガバナンススタイルガイドに追加することをお勧めします。これらの2つのルールは、コンテンツなしでエラーを定義した場合や、エラー詳細を提供するのに予期しない形式を使用した場合にフィードバックを提供します。

  # Author: Frank Kilcommins (https://github.com/frankkilcommins) 
  no-errors-without-content:
    message: Error responses MUST describe the error
    description: Error responses should describe the error that occurred. This is useful for the API consumer to understand what went wrong and how to fix it. Please provide a description of the error in the response.
    given: $.paths[*]..responses[?(@property.match(/^(4|5)/))]
    then:
      field: content
      function: truthy
    formats: [oas3]
    severity: warn

  # Author: Phil Sturgeon (https://github.com/philsturgeon)
  no-unknown-error-format:
      message: Error response should use a standard error format.
      description: Error responses can be unique snowflakes, different to every API, but standards exist to make them consistent, which reduces surprises and increase interoperability. Please use either RFC 7807 (https://tools.ietf.org/html/rfc7807) or the JSON:API Error format (https://jsonapi.dokyumento.jp/format/#error-objects).
      given: $.paths[*]..responses[?(@property.match(/^(4|5)/))].content.*~
      then:
        function: enumeration
        functionOptions:
          values:
            - application/vnd.api+json
            - application/problem+json
            - application/problem+xml
      formats: [oas3]
      severity: warn

これらのルールが適用されると、Bookstore APIの`0.0.1`バージョンについて、以下の具体的なフィードバックが得られます。

63:15  warning  no-errors-without-content  Error responses MUST describe the error             paths./books.get.responses[400]
65:15  warning  no-errors-without-content  Error responses MUST describe the error             paths./books.get.responses[401]
70:30  warning  no-unknown-error-format    Error response should use a standard error format.  paths./books.get.responses[500].content.application/json
105:15  warning  no-errors-without-content  Error responses MUST describe the error             paths./orders.post.responses[401]
107:15  warning  no-errors-without-content  Error responses MUST describe the error             paths./orders.post.responses[422]
109:15  warning  no-errors-without-content  Error responses MUST describe the error             paths./orders.post.responses[500]

エラー応答を改善するにはどうすればよいですか?

初期設計における欠点がどこにあるかを把握できたので、エラー応答をどのように改善できるでしょうか?SwaggerHub Problem Detailsドメインは公開されているため、Bookstore APIのエラー応答を簡単に改善するために利用できます。

多くの場合、直接の応答と例は構造を一般的に表しているため、それらをそのまま活用できます。これは`POST /orders`リソースで行うことです。

/orders:
    post:
      summary: Place book order
      description: |
        This API method allows placing an order for one or more books
      operationId: createOrder
      tags:
      - Bookstore
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Order'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderDetails'
        '400':
          $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/BadRequest'
        '401':
          $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized'
        '422':
          $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ValidationError'
        '500':
          $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError'
        '503':
          $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable'

これにより、エラーのより豊かで明示的な表現が得られます。

他の状況では、おそらく特定のエラー例のみが適用されるため(または独自の例を作成したい場合)、エラー応答を調整したい場合があります。これは、公開されているスキーマを活用しながらも可能であり、リクエストボディに関連する例は適用されないため、`GET /books`パスに必要です。以下に、再利用可能なドメインによって公開されているスキーマを活用しながら、応答のコンテンツとエンコーディングを設定します。また、パスに適用される例も明示的に参照します。

responses:
        '200':
          $ref: '#/components/responses/books'
        '400':
          description: |
            The request was malformed or could not be processed.
    
            Examples of `Bad Request` problem detail responses:

             - [Missing request parameter](https://problems-registry.smartbear.com/missing-request-parameter/)
             - [Invalid request parameter format](https://problems-registry.smartbear.com/invalid-request-parameter-format/)
             - [Invalid request parameter value](https://problems-registry.smartbear.com/invalid-request-parameter-value/)
          content:
            application/problem+json:
              schema:
                $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/schemas/ProblemDetails'
              examples:
                missingRequestParameterWithErrors:
                  $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/examples/missing-request-parameter-with-errors'
                invalidRequestParameterFormatWithErrors:
                  $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/examples/invalid-request-parameter-format-with-errors'
                invalidRequestParameterValueWithErrors:
                 $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/examples/invalid-request-parameter-value-with-errors'
        '401':
          $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/Unauthorized'
        '500':
          $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServerError'
        '503':
          $ref: 'https://api.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0#/components/responses/ServiceUnavailable'

改善されたBookstore APIは、SwaggerHub [7]で直接確認できます。

実際の例

多くのAPIプロバイダー、ツールベンダー、およびプログラミングフレームワークによる標準の採用は心強いです。

以下に、すでに標準を採用しているSmartBear APIのいくつかを示します。

結論

提供されたリソースと例により、APIでProblem Detailsの実装を開始する準備が整っています。公開されているドメインとレジストリを活用して、作業を加速し、初期費用を削減し、チームが車輪を再発明する必要がないようにすることをためらわないでください。さらに重要なことは、これらのツールがAPI環境全体で一貫性を維持し、エンドユーザーと開発者の両方にとってエラー処理エクスペリエンスを向上させるのに役立つことです。

参考文献 説明 URL
[1] RFC 9457標準ドキュメント https://www.rfc-editor.org/info/rfc9457
[2] 問題タイプのIANAレジストリ https://www.iana.org/assignments/http-problem-types
[3] SmartBear 問題レジストリ https://problems-registry.smartbear.com/
[4] Problem Details用のSwaggerHubドメイン https://app.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0
[5] JSONスキーマドラフト2019-09 https://json-schema.dokyumento.jp/draft/2019-09/schema
[6] SmartBear Problems RegistryのGitHubリポジトリ https://github.com/SmartBear-DevRel/problems-registry
[7] 書店API https://app.swaggerhub.com/apis-docs/frank-kilcommins/Bookstore-API/1.0.0