29 July 2016 —
The other day I was working on a Zend Expressive application I’m currently building. The application includes a REST API among other things, but it also has some endpoints which render HTML.
In one of my tests of the REST API I saw that when an error occurs (404, 405 or 500), I was getting an HTML response, which is not easy to handle when the client is expecting JSON.
I started to dig on how to fix this problem and thought that using ErrorMiddleware (which is invoked in case of an error) should be the solution, but after some tests I saw that it is only invoked if a regular middleware invokes the next one by passing an error as the third argument or an uncaught exception is thrown.
When a route is not matched (404) or it is matched with an incorrect HTTP method (405), the error middleware is not invoked.
I asked on twitter if this was a bug or a feature, and Abdul Malik confirmed that it was the intended behavior.
Before that, Nikola Poša had already pointed me to the Final Handler documentation, which is the element in Zend Expressive responsible of catching unhandled errors and recover gracefully. I read the documentation again, in case I missed something.
When I told him that my intention was to return different content for errors based on what the client expects (the Accept
header), he kindly showed me one of his implementations to solve this problem, and it gave me some ideas on how to implement something myself.
Standard implementations
Well, Zend Expressive comes with some built-in Final Handler implementations. They are callables that get invoked with the request and response objects when no other middleware has returned a valid response or an exception is thrown, so that the application returns some kind of error instead of crashing.
To achieve this, any final handler has to return a response, which is the one that will be finally sent to the client.
The most simple one is provided by the zend-stratigility package, the Zend\Stratigility\FinalHandler
. It basically returns a plain text response with the error, but includes the correct status code in the response (404, 500, etc).
Since that is too simple for most applications, Zend Expressive includes two other Final Handlers, the Zend\Expressive\TemplatedErrorHandler
and the Zend\Expressive\WhoopsErrorHandler
.
The first one composes a template renderer, so that certain templates are rendered in case of error, returning a more human friendly error than the one returned by the stratigility’s FinalHandler.
The second one is intended to be used in development only, and returns very accurate information about any produced error, by using the whoops! package.
These two error handlers come preconfigured when you install the expressive skeleton application, and you can find more documentation about them here.
The problem to solve
The standard implementations are quiet useful, but none of them return JSON errors, so if you need JSON errors you have to write your own final handler.
This is my implementation. It is an early version and there is probably some ways to improve it, but it works:
It is a little bit coupled with my app at this moment, but this is how it works:
- It checks if a
Zend\Expressive\Router\RouteResult
was registered in the request. That means that this is not a 404 or 405 error, because the expressive’s routing middleware registers the RouteResult when a route is matched. - If no
Zend\Expressive\Router\RouteResult
is registered, we have to check if current error is a 404 or 405 status. In the second case, expressive passes an error, but in the first one it doesn’t. - In any other case we will use current response status if it is already an error status (>=400) or use the 500 status otherwise.
- Finally we compose a JsonResponse with the status code and the reason phrase.
That’s pretty simple. If we register now this as a dependency with the Zend\Expressive\FinalHandler name, it will get invoked when an error occurs.
However, this doesn’t solve our initial problem. Now the application instead of always rendering HTML errors, it always renders JSON errors. We have solved the problem of the REST API, but when the application is loaded in a web browser and an error occurs, the JSON response won’t make sense.
Content-based Error Handler
My final solution was using the strategy design pattern to decide which Error handler to use at runtime, based on the request’s Accept
header value.
For my implementation I’ve used a zend-servicemanager PluginManager, but this could be easily done without it.
Update 2016-07-30: In the first version of this article, the
ContentBasedErrorHandler
was aPluginManager
itself. If you read the comments, Nikola Poša suggested to split it into two elements, the ErrorHandler and the PluginManager, and make the first one encapsulate the second.
It is a much cleaner approach, and properly segregates the two responsibilities, so I have updated the example.
This error handler delegates the management of the error itself into another error handler by composing a plugin manager.
When the plugin manager is created, it has to receive the plugins configuration, which maps different content types to the error handler that will manage that specific content type.
For example, for text/html we will use the built-in Zend\Expressive\TemplatedErrorHandler
(or the Zend\Expressive\WhoopsErrorHandler
if we are in a development environment), but for application/json we will use the JsonErrorHandler
.
error-handler.global.php:
error-handler.local.php:
This way, if the application was loaded in a browser (which provides the Accept: text/html
header) and an error occurs, the client will see a pretty HTML error page.
On the other hand, if a REST client performs a request by passing the Accept: application/json
header, the error will be JSON-formatted, preventing the client application to crash because of a parsing error.
If you need any other content type to be managed by your application, you just need to write the specific Error Handler for that format and register it.
Finally, you will have to register the ContentBasedErrorHandler
with the Zend\Expressive\FinalHandler name, so that it is properly injected in the Application
when created.
And that’s it. This approach can be clearly improved, but it is good starting point.
Update 2016-08-12: I have finally created a package implementing this solution, so that anyone can install it in his/her own project: acelaya/ze-content-based-error-handler.