Generating Problem Details Responses

When writing middleware, you will often be able to detect error conditions within the middleware logic. When you do, you can immediately return a problem details response.

ProblemDetailsResponseFactory

This library provides a factory named ProblemDetails\ProblemDetailsResponseFactory. It defines two static methods, createResponse() and createResponseFromThrowable(). Each accepts the PSR-7 ServerRequestInterface instance as its first argument, and then additional arguments in order to create the response itself:

For createResponse(), the signature is:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

public function createResponse(
    ServerRequestInterface $request,
    int $status,
    string $detail,
    string $title = '',
    string $type = '',
    array $additional = []
) : ResponseInterface {

where:

The signature of createResponseFromThrowable() is:

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

public function createResponseFromThrowable(
    ServerRequestInterface $request,
    Throwable $e
) : ResponseInterface {

where:

Normal usage of the factory will use a response and a stream from zend-diactoros for the response prototype and response body, respectively; additionally, responses will not include exception details (file, line number, backtrace, etc.), and JSON responses will use a set of flags for generating human-readable JSON. If these defaults work for your needs, you can instantiate the factory directly in your code in order to generate a response:

// From scalar data:
$response = (new ProblemDetailsResponseFactory())->createResponse(
    $request,
    400,
    'Unrecognized fields present in request'
);

// From a throwable:
$response = (new ProblemDetailsResponseFactory())
    ->createResponseFromThrowable($request, $e);

More often, you will want to customize behavior of the factory; for instance, you may want it to act differently in development than in production, or provide an alternate PSR-7 implementation. As such, the constructor has the following signature:

use Psr\Http\Message\ResponseInterface;

public function __construct(
    bool $isDebug = ProblemDetailsResponseFactory::EXCLUDE_THROWABLE_DETAILS,
    int $jsonFlags = null,
    ResponseInterface $response = null,
    callable $bodyFactory = null
) {

where:

ProblemDetailsResponseFactoryFactory

This package also provides a factory for generating the ProblemDetailsResponseFactory for usage within dependency injection containers: ProblemDetails\ProblemDetailsResponseFactoryFactory. It does the following:

If any of the above are not present, a null value will be passed, allowing the default value to be used.

If you are using Expressive and have installed zend-component-installer in your application, the above factory will be wired already to the ProblemDetails\ProblemDetailsResponseFactory service via the provided ProblemDetails\ConfigProvider class.

Examples

Returning a Problem Details response

Let's say you have middleware that you know will only be used in a production context, and need to return problem details:

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use ProblemDetails\ProblemDetailsResponseFactory;
use Psr\Http\Message\ServerRequestInterface;

class ApiMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        // discovered an error, so returning problem details:
        return (new ProblemDetailsResponseFactory())->createResponse(
            $request,
            403,
            'You do not have valid credentials to access ' . $request->getUri()->getPath(),
            '',
            '',
            ['login' => '/login']
        );
    }
}

The above will return a JSON response if the Accept request header matches application/json or any application/*+json mediatype. Any other mediatype will generate an XML response.

Using a Throwable to create the response

Let's say you have middleware that invokes functionality from a service it composes, and that service could raise an exception or other Throwable. For this, you can use the createResponseFromThrowable() method instead.

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use ProblemDetails\ProblemDetailsResponseFactory;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;

class ApiMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        try {
            // some code that may raise an exception or throwable
        } catch (Throwable $e) {
            return (new ProblemDetailsResponseFactory())
                ->createResponseFromThrowable($request, $e);
        }
    }
}

As with the previous example, the above will return a JSON response if the Accept request header matches application/json or any application/*+json mediatype. Any other mediatype will generate an XML response.

By default, createResponseFromThrowable() will only use the exception message, and potentially the exception code (if it falls in the 400 or 500 range). If you want to include full exception details — line, file, backtrace, previous exceptions — you must pass a boolean true as the first argument to the constructor. In most cases, you should only do this in your development or testing environment; as such, you would need to provide a flag to your middleware to use when invoking the createResponseFromThrowable() method, or, more correctly, pass a configured ProblemDetailsResponseFactory instance to your middleware's constructor. As a more complete example:

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use ProblemDetails\ProblemDetailsResponseFactory;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;

class ApiMiddleware implements MiddlewareInterface
{
    private $problemDetailsFactory;

    public function __construct(
        /* other arguments*/
        ProblemDetailsResponseFactory $problemDetailsFactory)
    {
        // ...
        $this->problemDetailsFactory = $problemDetailsFactory;
    }

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        try {
            // some code that may raise an exception or throwable
        } catch (Throwable $e) {
            return $this->problemDetailsFactory
                ->createResponseFromThrowable($request, $e);
        }
    }
}

Creating Custom Response Types

If you have common problem types you will use over and over again, you may not wish to provide the type, title, and/or status each time you create the problem details. For those, we suggest creating extensions to ProblemDetailsResponseFactory. To use the example from the introduction, we could have a RateLimitResponse generated as follows:

use ProblemDetails\ProblemDetailsResponseFactory;
use Psr\Http\Message\ServerRequestInterface;

class RateLimitResponseFactory extends ProblemDetailsResponseFactory
{
    const STATUS = 403;
    const TITLE = 'https://example.com/problems/rate-limit-exceeded';
    const TYPE = 'You have exceeded the rate limit.';

    public function create(
        ServerRequestInterface $request,
        int $tries,
        int $rateLimit,
        int $expires
    ) {
        return self::createResponse(
            $request,
            self::STATUS,
            sprintf('You have exceeded your %d requests per hour rate limit', $rateLimit),
            self::TITLE,
            self::TYPE,
            [
                'requests_this_hour' => $tries,
                'rate_limit' => $rateLimit,
                'rate_limit_reset' => date('c', $expires),
            ]
        );
    }
}

You would then compose this alternate factory in your middleware, and invoke it as follows:

$this->rateLimitResponseFactory->create(
    $request,
    $tries,
    $rateLimit,
    $expires
);