In one of my Zend Expressive-based projects, among other things I needed to create a error handling implementation capable of processing exceptions and emitting them as errors in a format suitable for a client that initiated the request. In essence, I needed a mechanism that supports my preference of dealing with exceptional situations.

Default approach

Out of the box, Zend Expressive provides error handling mechanism based on the FinalHandler implementation. At first glance, it seems that it perfectly fits to my plan of having a single place in the application as a last resort for dealing with exceptions.

Two implementations are available: Zend\Expressive\TemplatedErrorHandler and Zend\Express\WhoopsErrorHandler, that provides integration with Whoops, popular error handling library. But none of them seemed to be ideal for my goal. Whoops itself has what I consider necessary by the mean of its handlers concept, but this Zend Expressive integration wasn't designed accordingly.

I eventually managed to come up with a satisfactory solution, but it required a lot of hacking in an effort to make Whoops and Zend Expressive work together the way I wanted to. Squaring the circle is perhaps the most appropriate analogy for describing this endeavor. And when I got back to my code after a while, I was horrified by a solution I had in front of me. Indeed, code you write today is already becoming legacy tomorrow. I went for a refactor...

Whoops-less solution

After I studied error middleware concept in more detail, I've realized that I could achieve the same goal without using Whoops, and even without FinalHandler mechanism. The key is in building error middleware pipeline and its prioritization. Here is the middleware configuration that I have in errorhandler.global.php:

'middleware_pipeline' => [
    'error_normalizer' => [
        'middleware' => [
            Dnbk\ErrorHandling\NotFoundNormalizerMiddleware::class,
        ],
        'priority' => -1000,
    ],
    'error' => [
        'middleware' => [
            Dnbk\ErrorHandling\ErrorLoggerMiddleware::class,
            Dnbk\ErrorHandling\ExceptionProcessorMiddleware::class,
            Dnbk\ErrorHandling\JsonErrorHandler::class,
            Dnbk\ErrorHandling\HtmlErrorHandler::class,
        ],
        'error' => true,
        'priority' => -9999,
    ],
],

The whole point is to define error middleware handlers, marked with 'error' => true configuration flag. Thereby, it is crucial to set a lowest possible priority so that they only get invoked when an exception is raised.

Consistent error treatment

Notice that before the actual error middleware pipeline, I defined regular NotFoundNormalizerMiddleware with the priority of -1000, which means that it will be executed long after Routing and Dispatching. Its purpose is to turn "Not found" responses into appropriate Exception, so that I have a consistent handling of exceptional situations, which was the main objective:

final class NotFoundNormalizerMiddleware
{
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
    {
        if ($response->getStatusCode() === 200 && $response->getBody()->getSize() === 0) { //404?
            throw new PageNotFoundException();
        }
    }
}

Intent for creating this normalizer is my belief that 404 Not Found is like any other 4xx client error, and thus should be treated the same way. By doing so, I have a much more consistent and unambiguous treatment of errors.

Content-based error handling

What I wanted to accomplish from the very beginning is having a JSON error response in case of AJAX-like requests and standard HTML error page in any other. Alejandro Celaya come up with a very nice content-based Error Handler solution, based on having a FinalHandler that inspects the Accept header and selects the appropriate strategy to handle error.

I was able to achieve the desired goal using a rather different approach which doesn't require FinalHandler, but a pure middleware solution based on chaining handlers in the error middleware pipeline.

In essence, I took advantage of a basic middleware concept to line up the execution of error middleware handlers, whereas HtmlErrorHandler is used as a last resort and it won't be reached if JsonErrorHandler it detects AJAX request thus returns JSON response:

final class JsonErrorHandler implements ErrorMiddlewareInterface
{
    public function __invoke($error, ServerRequestInterface $request, ResponseInterface $response, callable $out = null)
    {
        $serverParams = $request->getServerParams();

        if (empty($serverParams['HTTP_X_REQUESTED_WITH']) || strtolower($serverParams['HTTP_X_REQUESTED_WITH']) != 'xmlhttprequest') {
            return $out($request, $response, $error);
        }

        return new JsonResponse([
            'error' => $error->getMessage(),
            'code'  => $error->getCode(),
        ], (int) $error->getCode());
    }
}
final class HtmlErrorHandler implements ErrorMiddlewareInterface
{
    /**
     * @var TemplateRendererInterface
     */
    private $templateRenderer;

    /**
     * @var string
     */
    private $templates;

    public function __construct(TemplateRendererInterface $templateRenderer, array $templates)
    {
        $this->templateRenderer = $templateRenderer;
        $this->templates = $templates;
    }

    public function __invoke($error, ServerRequestInterface $request, ResponseInterface $response, callable $out = null)
    {
        $status = $error->getCode();

        $data = [
            'error' => $error->getMessage(),
            'status' => $status,
        ];

        $template = isset($this->templates[$status])
            ? $this->templates[$status]
            : $this->templates['error'];

        $response->getBody()->write(
            $this->templateRenderer->render($template, $data)
        );

        return $response;
    }
}

Besides this two handlers, I also implemented one that logs raised Exception - ErrorLoggerMiddleware and one that processes it - ExceptionProcessorMiddleware, in order to mark it with the appropriate HTTP status code before it reaches handlers that send error response.

Closing remarks

Middleware concept works nicely for both normal request lifecycle as well as for exceptional situations. No extra customization is needed for implementing different strategies for error handling.

Recently, none other than Matthew Weier O'Phinney sent this intriguing tweet:

It made me curious how that new error handling implementation will look like and whether some of the problems I faced with the current implementation will be tackled.