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:
Also in camping productivity: prototyped new error handling for Stratigility and Expressive. As in completely middleware based...
— weierophinney (@mwop) August 24, 2016
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.
Comments