Problem Details (RFC 9457): Getting Hands-On with API Error Handling

  May 02, 2024

Welcome back! Let’s dive right into the world of API error handling with the Problem Details standard. In Part I, we unpacked several challenges arising from inconsistent error reporting—challenges that amplify as the digital ecosystem expands through the production of more APIs. From the criticality of delivering actionable error messages to the pitfalls of custom error methods and security vulnerabilities, we saw how RFC 9457's Problem Details provides a robust framework to standardize API error responses.

We highlighted the evolution from RFC 7807 to RFC 9457, including significant advancements in the articulation of API errors, turning error handling from a developer's nightmare into a streamlined, informative, and actionable process. This standard not only makes error handling more intuitive for developers, but also enhances security and consistency across digital platforms.

Now, with a solid understanding of the standard's benefits and framework, let’s transition into practical implementations. This segment will guide you through leveraging Problem Details within your APIs, using real-world examples and resources to ensure your systems deliver 'bad news' in the best possible way.

Getting Started with Problem Details

With the introduction of any standard into a team or organization, getting started is often the hardest step. Start by familiarizing yourself with RFC 9457 [1].

Don’t Start from Scratch

To avoid common pitfalls and accelerate the adoption of Problem Details take advantage of the following resources and artifacts which will bootstrap your adoption and set you on a course to deliver consistent error handling with your teams.

Registries

As mentioned in Part I of the series, the new RFC 9457 introduces the concept of a registry of common problem type URIs. The formal registry hosted at IANA [2] as well as the SmartBear Problems Registry [3] act as valuable resources when determining what problem types are relevant for your APIs.

  • IANA Registry: The formal registry holds standardized problem type URIs that you can use out-of-the-box or as references when defining your own problem types.
  • SmartBear Problems Registry: This registry offers a catalog of common problem types specific to various API scenarios curated by the SmartBear Team. In future, some may migrate to the IANA registry.

Objects, Schemas, and Extensibility

The benefits of standards are we can circumvent much of the bike-shedding associated with the shape of error details. The RFC provides the following non-normative JSON Schema for HTTP Problem Details which guarantees the base shape of errors.

In situations where you need to provide more information than covered by the JSON Schema above, rest assured the built-in extensibility is a powerful mechanism allowing you to mold the standard to meet the needs of your team(s).

Extensibility brings its own challenges. Therefore, it’s good practice to clearly define your extension points and communicate to clients consuming problem details that they MUST ignore extensions they do not recognize. To make the process predictable for those implementing (and indeed those consuming the responses), I would also recommend crafting a JSON Schema including your extensions.

Here’s a JSON Schema [5] including the errors and code extensions we use for problem details within new APIs at SmartBear:

{
    "$schema": "https://json-schema.org/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"
    ]
  }

This gives a powerful and detailed ability to describe the occurrence of an error related to parameters or a request body.

Let’s run through a few examples based on the above JSON Schema

  1. For an issue with a missing request parameter (e.g. a query parameter), we can leverage the errors extension to provide explicit information on the missing parameter via the details and parameter properties
{
    "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. For an issue with a malformed request body property, we can leverage the errors extension to provide explicit information on the issue as well as the property location via the details and pointer (which specifies a JSON Pointer to the location of the property) properties
{
    "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. If we find multiple errors, and want to return all violations to the client rather than forcing an over chatty type of engagement, we can leverage the errors array extension to include details on all applicable errors for the associated problem type
{
    "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"
        }
    ]
}

Accelerate with a Ready to Use Domain for OpenAPI

To make it even easier to adopt problem details into your next API project, I’ve wrapped up the assets above into a ready to use SwaggerHub domain [4], which can be referenced from various parts of your OpenAPI descriptions.

The domain comprises of:

  • Schemas: the full and extended (a.k.a. opinionated) schemas for HTTP Problem Details

  • Examples: A rich list of representative response examples for the supported problem types

  • Responses: A ready to reference list of OpenAPI compatible responses for the supported problem types

I’ll walk through an example of leveraging the free and public domain within an OpenAPI description in the section below.

Using Problem Details with OpenAPI

Leveraging Problem Details within an OpenAPI description is easier than you think, and it’s made event simpler by leveraging some of the above artifacts. Let’s create an OpenAPI description for a simple Bookstore API to showcase can help improve the error responses within an API design.

In the first pass through authoring the OpenAPI description, I’ll setup the base objects (Info, Tags, Servers), two resources for retrieving books and placing a book order, and the relevant schemas for the books and orders resources.

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: http://swagger.io/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: []

This simple OpenAPI description gives me the following interactive API documentation which one might consider robust as it covers multiple errors responses. However, it lacks critical elements to help surface the potential errors that can occur with the API.

How can I catch inconsistent errors?

To help ensure that we do not forget to apply the HTTP Problem Details standard within our APIs, I would recommend adding the following Spectral rules to your governance style guides. These two rules will provide feedback if you define errors without any content or if you use an unexpected format to serve the error details:

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

With these rules in place, I get the following opinionated feedback on my 0.0.1 version of the Bookstore API:

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]

How can I improve the error responses?

Now that I have a handle on where the shortcomings are in my initial design, how can I improve the error responses? As the SwaggerHub Problem Details domain is published publicly, I can take advantage to easily improve the error responses in my Bookstore API.

In many scenarios, it’s possible to just leverage the direct responses and examples are they generically represented the structure. This is what I do for the POST /orders resource:

/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'

This results in a much richer and explicit representation of the errors:

In other situations, perhaps you want to tailor the error response as only certain examples might be applicable (or maybe you want to craft your own example). This is still possible while leveraging the exposed schemas, and it’s what I need for the GET /books path as examples related to a request body are not applicable. Below, I set the content and encoding for the response while still leveraging the schema exposed by the reusable domain. I also explicitly reference the examples that are applicable for the path.

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'

The improved Bookstore API can be viewed directly in SwaggerHub [7].

Examples in the Wild

It’s encouraging to see the adoption of the standard by many API providers, tooling vendors, and programming frameworks.

Here’s some SmartBear APIs that have already adopted the standard:

Conclusion

With the resources and examples provided, you’re well-equipped to start implementing Problem Details in your APIs. Don't hesitate to leverage the public domains and registries to accelerate your journey, reduce initial overheads, and ensure that your team doesn't have to reinvent the wheel. More importantly, these tools help maintain consistency across your API landscape, improving the error handling experience for end-users and developers alike.

Ref Description URL
[1] RFC 9457 Standard Document https://www.rfc-editor.org/info/rfc9457
[2] IANA Registry of Problem Types https://www.iana.org/assignments/http-problem-types
[3] SmartBear Problems Registry https://problems-registry.smartbear.com/
[4] SwaggerHub Domain for Problem Details https://app.swaggerhub.com/domains/smartbear-public/ProblemDetails/1.0.0
[5] JSON Schema Draft 2019-09 https://json-schema.org/draft/2019-09/schema
[6] GitHub Repository for SmartBear Problems Registry https://github.com/SmartBear-DevRel/problems-registry
[7] Bookstore API https://app.swaggerhub.com/apis-docs/frank-kilcommins/Bookstore-API/1.0.0