问题详情 (RFC 9457):动手实践 API 错误处理

  May 02, 2024

欢迎回来!让我们直接深入 API 错误处理的世界,了解问题详情标准。在第一部分中,我们剖析了由不一致的错误报告引发的若干挑战——随着更多 API 的生产,数字生态系统不断扩张,这些挑战也随之加剧。从提供可操作错误消息的重要性,到自定义错误方法和安全漏洞的陷阱,我们看到了 RFC 9457 的问题详情如何提供了一个强大的框架来标准化 API 错误响应。

我们强调了从 RFC 7807 到 RFC 9457 的演变,其中包括 API 错误表述的重大进展,将错误处理从开发者的噩梦转变为一个精简、信息丰富且可操作的过程。此标准不仅使开发者的错误处理更加直观,还增强了数字平台的安全性和一致性。

现在,在对标准的好处和框架有了扎实理解之后,让我们转入实际应用。本节将指导您如何在 API 中利用问题详情,通过真实的示例和资源确保您的系统以最佳方式传递“坏消息”。

问题详情入门

在团队或组织中引入任何标准时,通常最难的一步就是开始。请先熟悉 RFC 9457 [1]

不要从零开始

为了避免常见的陷阱并加速问题详情的采用,请利用以下资源和工件,它们将帮助您快速上手,并使您的团队能够提供一致的错误处理。

注册中心

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

  • IANA 注册中心该官方注册中心包含了标准化的、可直接使用的或可作为定义您自己的问题类型时参考的问题类型 URI。
  • SmartBear 问题注册中心该注册中心提供了 SmartBear 团队整理的、针对各种 API 场景的常见问题类型目录。将来,其中一些可能会迁移到 IANA 注册中心。

对象、模式和可扩展性

标准的好处在于,我们可以避免与错误详情的形状相关的许多“细枝末节”讨论。RFC 提供了以下非规范性的 HTTP 问题详情 JSON Schema,它保证了错误的基本形状。

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

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

这是在 SmartBear 新 API 中用于问题详情的 JSON Schema [5],其中包含 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 Schema 来看几个例子

  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.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 问题详情标准,我建议将以下 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 问题详情域已公开,我可以利用它轻松改进图书商店 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 中开始实施问题详情。请毫不犹豫地利用公共域和注册中心来加速您的进程,减少初始开销,并确保您的团队不必重复造轮子。更重要的是,这些工具有助于保持整个 API 生态系统的一致性,从而改善最终用户和开发者的错误处理体验。

© . All rights reserved.