问题详情 (RFC 9457):亲身体验 API 错误处理

  2024年5月2日

欢迎回来!让我们深入了解使用“问题详情”标准的 API 错误处理世界。在第一部分中,我们剖析了不一致的错误报告所带来的几个挑战——随着数字生态系统通过生产更多 API 而扩展,这些挑战也随之放大。从传递可操作的错误消息的关键性到自定义错误方法和安全漏洞的陷阱,我们看到RFC 9457 的“问题详情”提供了一个强大的框架来标准化 API 错误响应。

我们强调了从 RFC 7807 到 RFC 9457 的演变,包括在 API 错误表达方面的重大进展,将错误处理从开发人员的噩梦转变为简化的、信息丰富的和可操作的过程。该标准不仅使开发人员的错误处理更加直观,还提高了数字平台的安全性和一致性。

现在,在对该标准的优势和框架有了扎实的了解之后,让我们过渡到实际实现。本节将指导您在 API 中利用“问题详情”,使用真实的示例和资源,以确保您的系统以最佳方式传递“坏消息”。

“问题详情”入门

将任何标准引入团队或组织时,入门通常是最困难的一步。首先熟悉 RFC 9457 [1]

不要从头开始

为避免常见陷阱并加速采用“问题详情”,请利用以下资源和工件,它们将引导您进行采用,并让您走上与团队一起提供一致的错误处理的道路。

注册表

如本系列第一部分所述,新的 RFC 9457 引入了通用问题类型 URI 注册表的概念。在确定哪些问题类型与您的 API 相关时,在 IANA [2] 上托管的正式注册表以及SmartBear 问题注册表 [3] 都是宝贵的资源。

  • IANA 注册表正式注册表保存了标准化的问题类型 URI,您可以使用它们开箱即用,或者在定义自己的问题类型时作为参考。
  • SmartBear 问题注册表该注册表提供了由 SmartBear 团队策划的针对各种 API 场景的常见问题类型目录。将来,其中一些可能会迁移到 IANA 注册表。

对象、架构和可扩展性

标准的优点是我们可以避免与错误详细信息的形状相关的许多无谓争论。RFC 提供了以下非规范性的 HTTP 问题详情的 JSON 架构,它保证了错误的基本形状。

在您需要提供比上述 JSON 架构涵盖的更多信息的情况下,请放心,内置的可扩展性是一种强大的机制,允许您调整标准以满足您的团队的需求。

可扩展性带来了自身的挑战。因此,最佳实践是清楚地定义您的扩展点,并告知使用问题详情的客户端,他们必须忽略他们无法识别的扩展。为了使那些实施(以及确实使用响应的那些)的过程可预测,我还建议创建一个包含您的扩展的 JSON 架构。

这是一个JSON 架构 [5],其中包括我们在 SmartBear 的新 API 中用于问题详情的 errorscode 扩展

{
    "$schema": "https://json-schema.fullstack.org.cn/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 描述时,我将设置基本对象(Info、Tags、Servers),两个用于检索图书和下订单的资源,以及图书和订单资源的相关模式。

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.org.cn/terms/contact: 
    name: DevRel at SmartBear
    email: [email protected]license: 
    name: Apache 2.0
    url: https://apache.ac.cn/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 规则。这两个规则会在您定义没有内容的错误或使用意外格式来提供错误详细信息时提供反馈。

  # 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.fullstack.org.cn/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

有了这些规则,我就可以在我书店 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 域是公开发布的,因此我可以利用它来轻松改进我的书店 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'

改进后的书店 API 可以直接在 SwaggerHub [7] 中查看。

实际应用中的示例

看到许多 API 提供商、工具供应商和编程框架采用该标准令人鼓舞。

以下是一些已经采用该标准的 SmartBear API

结论

凭借所提供的资源和示例,您完全有能力开始在您的 API 中实现 Problem Details。请随时利用公共域和注册表来加速您的旅程,减少初始开销,并确保您的团队不必重新发明轮子。更重要的是,这些工具可以帮助保持 API 环境的一致性,从而改善最终用户和开发人员的错误处理体验。