How to customize "not found" and "method not allowed" response prototypes in Zend expressive 2

Sometimes the nature of an application requires you to change the default framework's way to structure error responses (like 404 and 405).

On this article I'm going to explain how to customize those responses when working with Zend Expressive 2.

Expressive 1. Error handler.

In Expressive 1, error handling was different.

There used to be an element called error handler, which was responsible of handling uncaught exceptions, but also, other errors, like the ones above (404 and 405).

When using the error handler it was easier to have an standard way to generate error responses for any kind of situation, like unmatched routes (404), routes requested with an incorrect HTTP verb (405) or uncaught exceptions (500), as well as other errors.

Expressive 2 behavior.

In expressive 2, the error handler is gone, replaced by a middleware which catches exceptions and lets you generate error responses. However, this middleware does not deal with 404 and 405 errors anymore. Now, those errors are handled by two new elements.

  • 405: The routing middleware (which is now a separated middleware class) returns a 405 response when it detects that a request has been performed to an existing path but using an unsupported method. It adds an Allow header too, including the allowed HTTP verbs for that path.
  • 404: The last middleware in the stack now receives the so called "default delegate", which is responsible of generating a response when the stack is exhausted. The default implementation is the NotFoundDelegate, which as its name suggests, returns a response with status 404.

The problem with this two elements is the response body.

The first one returns an empty body by default, and the second one returns an error template (if a renderer service was registered) or a plain text response otherwise.

If your API is returning JSON responses (for example), this behavior is inconsistent and should be fixed.

Customize the response prototype

Both the RouteMiddleware and the NotFoundDelegate allow a PSR-7 response to be injected on them, to be used as the response prototype.

This way, we can define a consistent response that has the same structure regardless the error.

- 404 -

For the "not found" error, we have to define and register our own factory for the default delegate, which gets injected our customized response prototype:

<?php
namespace App\Delegate;

use Psr\Container\ContainerInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Delegate\NotFoundDelegate;

class DefaultDelegateFactory
{
    public function __invoke(ContainerInterface $container)
    {
        $myResponsePrototype = new JsonResponse([
            'foo' => 'bar',
            'message' => 'Not found',
        ], 404); // We don't really need to set the status, since the NotFoundDelegate will set it for us

        return new NotFoundDelegate($myResponsePrototype);
    }
}

Now register it on the config/autoload/dependencies.global.php file:

<?php

use App\Delegate\DefaultDelegateFactory;

return [

    'factories' => [
        'Zend\Expressive\Delegate\DefaultDelegate' => DefaultDelegateFactory::class,
    ],

    // [...]

];

And that should be it.

Of course, in this case that we always want to return the same response, we could just define another delegate which returns it, instead of using the built in implementation.

- 405 -

The "method not allowed" case is very similar, but with the RouteMiddleware instead of the NotFoundDelegate. However, there is one thing to take into account that we'll see later.

First, let's create our own factory:

<?php
namespace App\Middleware;

use Psr\Container\ContainerInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Middleware\RouteMiddleware;
use Zend\Expressive\Router\RouterInterface;

class RouteMiddlewareFactory
{
    public function __invoke(ContainerInterface $container)
    {
        $myResponsePrototype = new JsonResponse([
            'bar' => 'foo',
            'message' => 'Method not allowed',
        ], 405); // Again, we don't need to set the status because the RouteMiddleware will set it

        return new RouteMiddleware($container->get(RouterInterface::class), $myResponsePrototype);
    }
}

Now we should register the middleware service and pipe it, but here's the "problem".

When doing this, my first approach was trying to override the Application::ROUTING_MIDDLEWARE name, but it didn't work.

Internally, when expressive detects that middleware name, it instantiates a RouteMiddleware instance of its own, without the custom response prototype.

I have already created a question in ZF's forum, because I'm not sure if that's the intended behavior: https://discourse.zendframework.com/t/customize-response-prototype-on-routemiddleware/129

In the meantime, we are going to use a different middleware/service name, so let's register it.

<?php

use App\Middleware\RouteMiddlewareFactory;

return [

    'factories' => [
       'my_routing_middleware' => RouteMiddlewareFactory::class, 
    ],

];

Now, we have to pipe our middleware instead of the built in Application::ROUTING_MIDDLEWARE. Do it on config\pipeline.php;

<?php
// [..]

// Remove this, which pipes the Application::ROUTING_MIDDLEWARE
//$app->pipeRoutingMiddleware();

// Add this instead so that our custom RouteMiddleware is used
$app->pipe('my_routing_middleware');

// [...]

And that's all.

If in the end, the "problem" described above is fixed somehow, I'll update this article.

Conclusion

This demonstrates how flexible expressive is.

We have many possible approaches. Overwrite built-in components, customize its behavior... We are always in control of the application, and there's no black magic involved.