Error handling for RESTful APIs

Introduction

When designing RESTful APIs, it is essential to pay sufficient attention to application error reporting. Simply returning a “400” (the HTTP equivalent of “an error has occurred”) is clearly not sufficient. Furthermore, it is important that error reporting is handled consistently across all RESTful APIs so that API consumers are not confronted with different mechanisms, structures, and naming conventions for different APIs.

The objective of this chapter is to introduce a standardized approach to error reporting.


A Standard: RFC-9457

The Dutch government API strategy designates the RFC-7807 standard as the basis for error reporting. However, this RFC has since been replaced by RFC-9457, and the recommendation to the sector is to follow this latest version. The differences between the two are minimal.

The content type to be used for error responses according to RFC-9457 is:

application/problem+json

The RFC-9457 standard defines a ‘Problem Details Record,’ which should include at least the following attributes:

Atttribute: Definition:
type A JSON string in the form of a URI reference that uniquely identifies the type of error. Different error types must have distinct URIs. If the URI is structured as a URL (i.e., starting with ‘http’ or ‘https’), it should ideally be dereferenceable, leading to a page with detailed information about the error type.
The type attribute may have the value “about:blank” if the HTTP status code itself is deemed sufficiently clear to serve as a response. However, this will rarely be the case.
Examples of type values include “Validation”, “StateConflict”, or “Access”.
See here for a proposal on structuring type fields.
title A short JSON string providing a human-readable description of the error type, such as “Unable to authenticate client” or “Parameter validation errors”.
The type and title fields are intrinsically linked, where type identifies the error category, and title provides a comprehensible name for it.
If type has the value about:blank, the title field is expected to contain the normative name of the associated HTTP status code (e.g., “Forbidden” for HTTP 403).
status A JSON number corresponding to the original HTTP status code generated by the server for the error. This code may differ from the actual HTTP response code due to modifications by routers, gateways, firewalls, or other intermediaries between the client and the server.

In addition to the required fields, the RFC-9457 standard defines several optional elements:

Attribute Definition
detail A JSON string containing a more detailed error message, providing the recipient with a clearer understanding of what went wrong. Note that including debug information such as stack traces is not allowed!
instance A unique identifier for this specific Problem Details Record. Each generated error must have a unique instance ID. The RFC-9457 standard states that this can be a locator leading to a page with information about the error instance. However, it may also be a URI that is relevant to the server but opaque to the client. The government standard prescribes using a UUID, and our proposal is to follow this approach. Example:
"instance": "urn:uuid:ebd2e7f0-1b27-11e8-accf-0ed5f89f718b"
The instance attribute can be used, for example, to locate the error record in a larger error log.

Example:

"type": https://cmf.energysector.nl/reference-data/error-catalogue[/{categorie}]/{fout-type}",
"title": "Description of the error",
"status": 401,
"detail": "More details regarding the error can be found here",
"instance": "urn:uuid:ebd2e7f0-1b27-11e8-accf-0ed5f89f718b"

General rules

When implementing a Problem Details record, it is important to remember that not everything necessarily requires a new type. For some error messages, the HTTP status code is already sufficiently descriptive, and the detail attribute can be used to provide a more specific error message. For example:

{
 "type": "https://cmf.energysector.nl/reference-data/error-catalogue/default/http-503",
 "title": "Service Unavailable",
 "status": 503,
 "detail": "Database cannot be reached"
}

In this case, the HTTP status code already conveys the core issue, while detail provides additional context. No additional attributes are required.

When defining new problem types, the following rules must be observed:

  1. Avoid exposing sensitive information in error messages, whether in human-readable text or additional attributes in the response payload, that could be exploited by third parties.
  2. Do not include implementation-specific or technical details such as stack traces in the response.
  3. Avoid defining problem types that are already clearly described by the HTTP status code. Example: A 403 Forbidden response for a PUT operation on a resource already clearly indicates that the resource cannot be overwritten. There is no need for an additional message stating the same. As a best practice, the type catalog should include standard entries for commonly used HTTP error codes to ensure consistent reporting.
  4. It is allowed to extend the standard with additional fields. The API standard makes use of this flexibility to add extra records, allowing for arbitrarily complex error messages to be constructed in a standardized way.

Error types

We should aim to define a limited number of error types. Consider the following list, where the HTTP status code itself already provides a hint:

Error Type Catalogue

Ideally, we should maintain a catalogue of error types. This catalogue should contain the following information:

1. Error Type

An URL that provides access to a specific entry in the catalogue. The format is:
https://<provider-host>/reference-data/error-catalogue/<category>/<error-type>

in more detail:

Field Definition
<provider-host> Identifies the owner of the catalogue, which may be a virtual hostname. The URL does not necessarily have to point to an actual internet resource, although the standard encourages this.
reference-data The error catalogue is part of a broader reference data catalogue.
error-catalogue Specifies that this catalogue is for recording error types.
<category> The error category, e.g., "default" for standard HTTP codes, "validation" for validation errors, "software" for software (processing) errors, etc.
<error-type> The specific name of the error type, e.g., "parameter-validation".

Example:

"https://cmf.energysector.nl/reference-data/error-catalogue/default/http-503"

2. Title

A textual description of the error type, readable by end users, which can be used as the title attribute in error responses.

3. HTTP Status

The HTTP error status code (or codes) that this error type applies to. Ideally, there should be a one-to-one mapping, but some types may be relevant for multiple status codes.

4. Detail Template

A string containing placeholders for parameters, such as:

"Conflicting parameters are: {param-list}"

This template, when expanded, provides the value for the detail field.

5. Extension Schema

A JSON schema that describes any extension attributes. This must align with an extension object as described in Enterprise Architect.


Generic ProblemDetails Type

Based on the above information, it is possible to define a generic model that can serve as the standard error response object for RESTful APIs:

ProblemDetails Standard model for problem details

The root of this model is a ProblemDetails class, which is essentially a one-to-one implementation of the model from the RFC-9457 standard. An additional field, traceID, has been added to facilitate faster and more efficient error tracing and to correlate related messages.

The traceID format is designed to be compatible with OpenTelemetry. It consists of the following components: \<version>-\<trace_id>-\<span_id>-\<trace_flags>

Example:

00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01

TraceID structure:

Component Definition
version Version of this trace format.
trace_id All messages that are part of the same trace (i.e., a transaction) share the same trace_id, logically linking them together.
span_id Within a trace, messages can be further linked using the span_id. Messages can also reference span_id as a parent identifier (e.g., "parent_id": "0x051581bf3cb55c13"), forming a hierarchical structure.
trace_flags Indicators that are passed along with the trace.

In principle, objects of the ProblemDetails type can be used directly as error responses, as they contain the relevant information needed for many generic errors. If additional details are required, an extension in the form of an array of ApplicationProblem objects can be used to provide feedback on application-specific errors.

Extension class: ApplicationProblem

An ApplicationProblem class can be used to report a specific category of errors. Each ApplicationProblem class can, in turn, include an optional list of ApplicationMessage classes, which are intended to provide additional details about the problem.

Attributes are defined as follows:

Attribute Definition
categoryCode An enumeration defining an application-specific error category. Examples include:
- Validation Errors
- Processing Errors
- Communication Errors
severityCode Indicates the severity of the error, ranging from Informational, to Warning, to Error, and finally to Critical Error.

Extension class: ApplicationMessage

Each category typically defines its own detail records, represented by objects of type ApplicationMessage, which include the following attributes:

Attribute Definition
identifier A unique identifier (application-specific) for the error type.
code An enumeration linking an application-specific error code to the identifier. Each ID has a corresponding unique error code.
text The actual message text.

Implicit Enumerations

The categoryCode and code attributes function as implicit enumerations (i.e., the schema does not explicitly define a fixed list of values). To still allow range validation, the enumeration type includes two additional attributes:


Example

Below is an example of a complete error response for a 400 Bad Request error.

The recommendation is to use type, title, and detail to provide as much generic, consistent, and reusable error information as possible (i.e., general error details), while using the ApplicationProblem extension record to return detailed information.

This approach allows for a relatively small set of error types to cover a wide variety of errors.

{
  "type": "https://cmf.energysector.nl/reference-data/error-catalogue/user-errors/parameter-validation",
  "title": "One or more parameters did not validate correctly.",
  "status": 400,
  "detail": "Conflicting parameters are: A, B, C, D",
  "instance": "urn:uuid:4017fabc-1b28-11e8-accf-0ed5f89f718b",
  "traceID": "00-80e1afed08e019fc1110464cfa66635c-7a085853722dc6d2-01",
  "applicationProblem": [
  {
    "categoryCode": "PARAMETER_VALIDATION_ERROR",
    "severityCode": "ERROR",
    "message": [
    {
      "identifier": "VAL01", 
      "code": "CONFLICT", 
      "text": "Parameters A and B cannot occur at the same time."
    },
    {
      "identifier": "VAL12",
      "code": "VALUE",
      "text": "Value of parameter C must be greater than value of parameter D."
    },
    {
      "identifier": "VAL12",
      "code": "VALUE", 
      "text": "Value of parameter B must be greater than 100"
    }]
  },
  {
    "categoryCode": "RESULT_SET_SIZE",
    "severityCode": "WARNING",
    "message": [
    {
      "identifier": "WRN03",
      "code": "RESULT_SET",
      "text": "Specified parameters may result in a very large result-set, not all results might be available."
    }]
  }]
}