Pragmatism in the real world

Rendering ApiProblem with PSR-7

In the API I’m currently building, I’m rendering errors using RFC 7807: Problem Details for HTTP APIs. As this is a Slim Framework project, it uses PSR-7, so I updated rka-content-type-renderer to support problem.

RFC 7807 defines a standard for sending details of an error in an HTTP response message. It supports both XML and JSON formats. From the RFC, an example response is:

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

{
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "balance": 30,
    "accounts": [
        "/account/12345",
        "/account/67890"
    ]
}

Only title and type are required, though status should also be set. Full information is in the RFC, which is one of the easier ones to read.

In PHP, Larry Garfield has created the crell/ApiProblem component. The code to implement the above message is:

Use Crell\ApiProblem\ApiProblem;

$problem = new ApiProblem("You do not have enough credit.", "http://example.com/probs/out-of-credit");
$problem
  ->setDetail("Your current balance is 30, but that costs 50.")
  ->setInstance("http://example.net/account/12345/msgs/abc");

$problem['balance'] = 30;
$problem['accounts'] = array(
  "http://example.net/account/12345",
  "http://example.net/account/67890"
);

$jsonString = $problem->asJson();
$xmlString = problem->asXml();

ApiProblemRenderer

The only tricky bit is working out if we need to send back JSON or XML. This is called content negotiation and we read the Accept header to find out what the client wants. However, the Accept header has a complicated format with quality levels as you can see from RFC 7321, section 5.3.2. Fortunately, we can use Negotiation by Will Durand to deal with this, which is how rka-content-type-renderer works.

rka-content-type-render now has an ApiProblemRenderer which will read the Accept header and work out the if the client would prefer JSON or XML. If it can’t determine, it will default to JSON.

In Expressive or Slim, it’s used like this:

use Crell\ApiProblem\ApiProblem;
use RKA\ContentTypeRenderer\ApiProblemRenderer;
use Zend\Diactoros\Response;

$app->get('/', function ($request, $response, $next) {
    $problem = new ApiProblem(
        'Unauthorised',
        'http://www.example.com/api/docs/authentication'
    );
    $problem->setStatus(403);

    $renderer = new ApiProblemRenderer();
    return $renderer->render($request, new Response(), $problem);
});

This is it in action, with an XML accept header:

$ curl -i -H "Accept: application/vnd.akrabat.api+xml" http://localhost:8888/
HTTP/1.1 403 Forbidden
Host: localhost:8888
Connection: close
X-Powered-By: PHP/7.0.14
Content-type: application/problem+xml

<?xml version="1.0"?>
<problem>
  <title>Unauthorised</title>
  <type>http://www.example.com/api/docs/authentication</type>
  <status>403</status>
</problem>

And with a JSON one:

$ curl -i -H "Accept: application/vnd.akrabat.api+json" http://localhost:8888/
HTTP/1.1 403 Forbidden
Host: localhost:8888
Connection: close
X-Powered-By: PHP/7.0.14
Content-type: application/problem+json

{
    "title": "Unauthorised",
    "type": "http://www.example.com/api/docs/authentication",
    "status": 403
}

Ideally, the client should specify application/problem+json in their accept header, but in practice, I’ve never seen that happen, which is why rka-content-type-renderer works out the preferred format based on the media types specified.