Error Reporting
HAL does a great job of defining a generic mediatype for resources with relational links. However, how do you go about reporting errors? HAL is silent on the issue.
REST advocates indicate that HTTP response status codes should be used, but little has been done to standardize on the response format.
For JSON APIs, though, two formats are starting to achieve large adoption:
application/vnd.error+json
and application/problem+json
. Apigility provides
support for the latter, which goes by the cumbersome title of Problem Details for HTTP
APIs; Apigility refers to it as
API Problem
and provides support for it via the
zf-api-problem module.
API Problem
API Problem goes by the mediatype application/problem+json
; interestingly, there is also an XML
variant, though Apigility does not provide support for it at this time.
The payload of an API Problem has the following structure:
- type: a URL to a document describing the error condition (optional, and "about:blank" is assumed if none is provided; should resolve to a human-readable document; Apigility always provides this).
- title: a brief title for the error condition (required; and should be the same for every problem of the same type; Apigility always provides this).
- status: the HTTP status code for the current request (optional; Apigility always provides this).
- detail: error details specific to this request (optional; Apigility requires it for each problem).
- instance: URI identifying the specific instance of this problem (optional; Apigility currently does not provide this).
As an example payload:
HTTP/1.1 500 Internal Error
Content-Type: application/problem+json
{
"type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
"detail": "Status failed validation",
"status": 500,
"title": "Internal Server Error"
}
You are not limited to the variables listed above! The API Problem specification allows you to compose any other additional fields that you feel would help further clarify the problem and why it occurred. Apigility uses this fact to provide more information in several ways:
- Validation error messages are reported via a
validation_messages
key. - When the
display_exceptions
view configuration setting is enabled, stack traces are included viatrace
andexception_stack
properties.
As an example, let's say a user hits an API service that requires authentication, but has not provided credentials (pretend for a moment that Apigility does not provide authentication and authorization). You could provide the URI to the end-user via the API Problem:
{
"type": "/apigility/documentation/Status-v2#oauth",
"detail": "Service requires authenticated user",
"status": 403,
"title": "Unauthorized",
"authentication_uri": "/oauth"
}
In the above, the authentication_uri
property is being used to hint to the consumer where they
should go to authenticate before trying the URI again.
Sending an API Problem Response
Apigility is built on top of Zend Framework 2, which means that it
inherits ZF2's MVC. As such, in the code you write, you can typically return a
Zend\Http\Response
object in order to halt execution and finish the request/response lifecycle.
If you want to return an API Problem, Apigility offers a specialized response object you can use:
the ZF\ApiProblem\ApiProblemResponse
from zf-api-problem.
This requires passing a ZF\ApiProblem\ApiProblem
object to the constructor. You can create both at
the same time, and immediately return them:
return new \ZF\ApiProblem\ApiProblemResponse(
new \ZF\ApiProblem\ApiProblem(400, 'The request you made was malformed')
);
Apigility will use the status code you provide to the ApiProblem
instance (the first argument in
the example above) as the HTTP response status, and then serialize the instance to provide the
problem details payload.
Exceptions
Another way to return an API Problem is by throwing an exception from within your code. Assuming the
Accept
header indicates a JSON representation, any exception thrown will be cast to an API
Problem:
- The exception message becomes the API Problem detail.
- The exception code, if it falls in a valid range for HTTP, becomes the HTTP status code.
This is perhaps the easiest and most portable way to short-circuit execution and return a problem response.
zf-api-problem
also provides a specialized exception interface,
ZF\ApiProblem\Exception\ProblemExceptionInterface
, which, when implemented and used, allows you to
specify the API Problem type, title, and additional properties to compose. As an example,
ZF\ApiProblem\Exception\DomainException
is an implementation, which you could use to customize the
problem detail prior to throwing the exception:
$ex = new \ZF\ApiProblem\Exception\DomainException('The request you made was malformed', 400);
$ex->setType('/documentation/problems/malformed-request');
$ex->setTitle('Malformed Request');
$ex->setAdditionalDetails([
'missing-sort-direction' => 'The sort direction query string was missing and is required'
]);
throw $ex;
When the response for the above exception is returned, it would look like the following:
HTTP/1.1 500 Internal Error
Content-Type: application/problem+json
{
"type": "/documentation/problems/malformed-request",
"detail": "The request you made was malformed",
"status": 400,
"title": "Malformed Request",
"missing-sort-direction": "The sort direction query string was missing and is required"
}
Summary
We chose the Problem Details specification for its flexibility and simplicity. You can have your own custom error types, so long as you have a description of them to link to. You can provide as little or as much detail as you want, and even decide what information to expose based on environment (e.g., production vs development).
Apigility will use the Problem Details format whenever a request matching a JSON mediatype is made and an error occurs.