問題詳情 (RFC 9457):實際操作 API 錯誤處理

  2024 年 5 月 2 日

歡迎回來!讓我們直接深入探討使用問題詳情標準的 API 錯誤處理世界。在第一部分中,我們揭示了由不一致的錯誤報告所引起的幾個挑戰—隨著數位生態系統透過產生更多 API 而擴展,這些挑戰會加劇。從傳遞可操作錯誤訊息的關鍵性,到自訂錯誤方法的陷阱和安全性漏洞,我們看到了RFC 9457 的問題詳情如何提供一個健全的框架來標準化 API 錯誤回應。

我們強調了從 RFC 7807 到 RFC 9457 的演變,包括 API 錯誤表達方面的重大進展,將錯誤處理從開發人員的夢魘轉變為簡化、資訊豐富且可操作的流程。此標準不僅使開發人員的錯誤處理更直觀,還增強了跨數位平台的安全性和一致性。

現在,在對標準的好處和框架有充分了解後,讓我們轉向實際實作。此部分將引導您如何在 API 中運用問題詳情,使用真實世界的範例和資源,以確保您的系統以最佳方式傳遞「壞消息」。

問題詳情入門

將任何標準引入團隊或組織時,入門通常是最難的一步。首先熟悉RFC 9457 [1]

不要從頭開始

為了避免常見的陷阱並加速採用問題詳情,請利用以下資源和成品,這些資源和成品將引導您採用並設定您與團隊一起提供一致錯誤處理的途徑。

登錄檔

如本系列的第 I 部分所述,新的 RFC 9457 引入了常見問題類型 URI 登錄檔的概念。在 IANA [2] 上託管的正式登錄檔以及 SmartBear 問題登錄檔 [3] 作為決定哪些問題類型與您的 API 相關時的寶貴資源。

  • IANA 登錄檔正式登錄檔包含標準化的問題類型 URI,您可以使用這些 URI,或在定義自己的問題類型時作為參考。
  • SmartBear 問題登錄檔此登錄檔提供 SmartBear 團隊策劃的特定於各種 API 案例的常見問題類型目錄。未來,某些可能會遷移到 IANA 登錄檔。

物件、結構描述和可延伸性

標準的好處是我們可以規避許多與錯誤詳細資訊形狀相關的無意義爭論。RFC 提供以下非規範性的 HTTP 問題詳細資訊的 JSON 結構描述,保證了錯誤的基本形狀。

在您需要提供比上述 JSON 結構描述涵蓋的更多資訊的情況下,請放心,內建的可延伸性是一種強大的機制,可讓您塑造標準以滿足您團隊的需求。

可延伸性帶來了自身的挑戰。因此,良好的做法是清楚地定義您的延伸點,並告知使用問題詳細資訊的用戶端,他們必須忽略他們不認可的延伸。為了使實作過程(以及確實使用回應的過程)可預測,我也建議建立一個包含您延伸的 JSON 結構描述。

這是JSON 結構描述 [5],其中包含我們在 SmartBear 的新 API 中用於問題詳細資訊的 errorscode 延伸

{
    "$schema": "https://json-schema.dev.org.tw/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 延伸,透過 detailsparameter 屬性來提供有關遺失參數的明確資訊
{
    "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 延伸,透過 detailspointer 屬性(指定屬性位置的 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 問題詳細資訊的完整和延伸(又名主觀)結構描述

  • 範例:支援的問題類型代表性回應範例的豐富列表

  • 回應:支援的問題類型適用的 OpenAPI 相容回應的現成參考列表

我將在下面的章節中逐步說明如何在 OpenAPI 描述中利用免費且公開的 網域

將問題詳情與 OpenAPI 搭配使用

在 OpenAPI 描述中利用問題詳情比您想像的要容易,而且透過利用上述某些成品,它變得更加簡單。讓我們為一個簡單的書店 API 建立一個 OpenAPI 描述,以展示如何幫助改善 API 設計中的錯誤回應。

在撰寫 OpenAPI 描述的第一步中,我將設定基本物件(資訊、標籤、伺服器)、用於檢索書籍和下訂單的兩個資源,以及書籍和訂單資源的相關結構描述。

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.dev.org.tw/terms/contact: 
    name: DevRel at SmartBear
    email: [email protected]license: 
    name: Apache 2.0
    url: http://www.apache.org/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 問題詳細資訊標準,我建議將以下 Spectral 規則新增到您的管理樣式指南中。如果您定義沒有任何內容的錯誤,或者如果您使用非預期的格式來提供錯誤詳細資訊,這兩個規則將提供意見反應

  # 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.dev.org.tw/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 問題詳情網域是公開發布的,因此我可以利用它來輕鬆改善我的書店 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'

這會產生更豐富且更明確的錯誤表示

在其他情況下,您可能想要客製化錯誤回應,因為可能只有某些範例適用(或者您可能想要建立自己的範例)。這在使用公開的 schema 時仍然是可行的,這也是我為 GET /books 路徑所需的功能,因為與請求主體相關的範例並不適用。以下,我設定了回應的內容和編碼,同時仍然利用可重複使用網域公開的 schema。我也明確地參考了適用於該路徑的範例。

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 環境中保持一致性,從而改善終端使用者和開發人員的錯誤處理體驗。

參考 描述 網址
[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] 用於問題詳情的 SwaggerHub 網域 https://app.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0
[5] JSON Schema 草案 2019-09 https://json-schema.dev.org.tw/draft/2019-09/schema
[6] SmartBear 問題註冊表的 GitHub 儲存庫 https://github.com/SmartBear-DevRel/problems-registry
[7] Bookstore API https://app.swaggerhub.com/apis-docs/frank-kilcommins/Bookstore-API/1.0.0