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
- 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"
}
]
}
- 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"
}
]
}
- 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 API
version: 0.0.1
description: |
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.0
paths:
/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_key
security:
- 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.