Error Handling

We recommend that your code raise exceptions for conditions where it cannot gracefully recover. Additionally, we recommend that you have a reasonable PHP error_reporting setting that includes warnings and fatal errors:

error_reporting(E_ALL & ~E_USER_DEPRECATED & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE);

If you follow these guidelines, you can then write or use middleware that does the following:

  • sets an error handler that converts PHP errors to ErrorException instances.
  • wraps execution of the delegate ($delegate->process()) with a try/catch block.

As an example:

function ($request, DelegateInterface $delegate)
{
    set_error_handler(function ($errno, $errstr, $errfile, $errline) {
        if (! (error_reporting() & $errno)) {
            // Error is not in mask
            return;
        }
        throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
    });

    try {
        $response = $delegate->process($request);
        return $response;
    } catch (Throwable $e) {
    } catch (Exception $e) {
    }

    restore_error_handler();

    $response = new TextResponse(sprintf(
        "[%d] %s\n\n%s",
        $e->getCode(),
        $e->getMessage(),
        $e->getTraceAsString()
    ), 500);
}

You would then pipe this as the outermost (or close to outermost) layer of your application:

$app->pipe($errorMiddleware);

So that you do not need to do this, we provide an error handler for you, via zend-stratigility: Zend\Stratigility\Middleware\ErrorHandler.

This implementation allows you to both:

  • provide a response generator, invoked when an error is caught; and
  • register listeners to trigger when errors are caught.

We provide the factory Zend\Expressive\Container\ErrorHandlerFactory for generating the instance; it should be mapped to the service Zend\Stratigility\Middleware\ErrorHandler.

We provide two error response generators for you:

  • Zend\Expressive\Middleware\ErrorResponseGenerator, which optionally will accept a Zend\Expressive\Template\TemplateRendererInterface instance, and a template name. When present, these will be used to generate response content; otherwise, a plain text response is generated that notes the request method and URI.

  • Zend\Expressive\Middleware\WhoopsErrorResponseGenerator, which uses whoops to present detailed exception and request information; this implementation is intended for development purposes.

Each also has an accompanying factory for generating the instance:

  • Zend\Expressive\Container\ErrorResponseGeneratorFactory
  • Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory

Map the service Zend\Expressive\Middleware\ErrorResponseGenerator to one of these two factories in your configuration:

use Zend\Expressive\Container;
use Zend\Expressive\Middleware;
use Zend\Stratigility\Middleware\ErrorHandler;

return [
    'dependencies' => [
        'factories' => [
            ErrorHandler::class => Container\ErrorHandlerFactory::class,
            Middleware\ErrorResponseGenerator::class => Container\ErrorResponseGeneratorFactory::class,
        ],
    ],
];

Use development mode configuration to enable whoops

You can specify the above in one of your config/autoload/*.global.php files, to ensure you have a production-capable error response generator.

If you are using zf-development-mode in your application (which is provided by default in the skeleton application), you can toggle usage of whoops by adding configuration to the file config/autoload/development.local.php.dist:

```php use Zend\Expressive\Container; use Zend\Expressive\Middleware;

return [ 'dependencies' => [ 'factories' => [ Middleware\WhoopsErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class, ], ], ]; ```

When you enable development mode, whoops will then be enabled; when you disable development mode, you'll be using your production generator.

If you are not using zf-development-mode, you can define a config/autoload/*.local.php file with the above configuration whenever you want to enable whoops.

Listening for errors

When errors occur, you may want to listen for them in order to provide features such as logging. Zend\Stratigility\Middleware\ErrorHandler provides the ability to do so via its attachListener() method.

This method accepts a callable with the following signature:

function (
    Throwable|Exception $error,
    ServerRequestInterface $request,
    ResponseInterface $response
) : void

The response provided is the response returned by your error response generator, allowing the listener the ability to introspect the generated response as well.

As an example, you could create a logging listener as follows:

namespace Acme;

use Exception;
use Psr\Log\LoggerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;

class LoggingErrorListener
{
    /**
     * Log format for messages:
     *
     * STATUS [METHOD] path: message
     */
    const LOG_FORMAT = '%d [%s] %s: %s';

    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function __invoke($error, ServerRequestInterface $request, ResponseInterface $response)
    {
        $this->logger->error(sprintf(
            self::LOG_FORMAT,
            $response->getStatusCode(),
            $request->getMethod(),
            (string) $request->getUri(),
            $error->getMessage()
        ));
    }
}

You could then use a delegator factory to create your logger listener and attach it to your error handler:

namespace Acme;

use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Zend\Stratigility\Middleware\ErrorHandler;

class LoggingErrorListenerDelegatorFactory
{
    /**
     * @param ContainerInterface $container
     * @param string $name
     * @param callable $callback
     * @return ErrorHandler
     */
    public function __invoke(ContainerInterface $container, $name, callable $callback)
    {
        $listener = new LoggingErrorListener($container->get(LoggerInterface::class));
        $errorHandler = $callback();
        $errorHandler->attachListener($listener);
        return $errorHandler;
    }
}

Handling more specific error types

You could also write more specific error handlers. As an example, you might want to catch UnauthorizedException instances specifically, and display a login page:

function ($request, DelegateInterface $delegate) use ($renderer)
{
    try {
        $response = $delegate->process($request);
        return $response;
    } catch (UnauthorizedException $e) {
    }

    return new HtmlResponse(
        $renderer->render('error::unauthorized'),
        401
    );
}

You could then push this into a middleware pipe only when it's needed:

$app->get('/dashboard', [
    $unauthorizedHandlerMiddleware,
    $middlewareThatChecksForAuthorization,
    $middlewareBehindAuthorizationWall,
], 'dashboard');

Default delegates

Zend\Expressive\Application manages an internal middleware pipeline; when you call $delegate->process(), Application is popping off the next middleware in the queue and dispatching it.

What happens when that queue is exhausted?

That situation indicates an error condition: no middleware was capable of returning a response. This could either mean a problem with the request (HTTP 400 "Bad Request" status) or inability to route the request (HTTP 404 "Not Found" status).

In order to report that information, Zend\Expressive\Application composes a "default delegate": a delegate it will invoke once the queue is exhausted and no response returned. By default, it uses a custom implementation, Zend\Expressive\Delegate\NotFoundDelegate, which will report a 404 response, optionally using a composed template renderer to do so.

We provide a factory, Zend\Expressive\Container\NotFoundDelegateFactory, for creating an instance, and this should be mapped to the Zend\Expressive\Delegate\NotFoundDelegate service, and aliased to the Zend\Expressive\Delegate\DefaultDelegate service:

use Zend\Expressive\Container;
use Zend\Expressive\Delegate;

return [
    'dependencies' => [
        'aliases' => [
            'Zend\Expressive\Delegate\DefaultDelegate' => Delegate\NotFoundDelegate::class,
        ],
        'factories' => [
            Delegate\NotFoundDelegate::class => Container\NotFoundDelegateFactory::class,
        ],
    ],
];

The factory will consume the following services:

  • Zend\Expressive\Template\TemplateRendererInterface (optional): if present, the renderer will be used to render a template for use as the response content.

  • config (optional): if present, it will use the $config['zend-expressive']['error_handler']['template_404'] value as the template to use when rendering; if not provided, defaults to error::404.

If you wish to provide an alternate response status or use a canned response, you should provide your own default delegate, and expose it via the Zend\Expressive\Delegate\DefaultDelegate service.

Page not found

Error handlers work at the outermost layer, and are used to catch exceptions and errors in your application. At the innermost layer of your application, you should ensure you have middleware that is guaranteed to return a response; this will prevent the default delegate from needing to execute by ensuring that the middleware queue never fully depletes. This in turn allows you to fully craft what sort of response is returned.

Generally speaking, reaching the innermost middleware layer indicates that no middleware was capable of handling the request, and thus an HTTP 404 Not Found condition.

To simplify such responses, we provide Zend\Expressive\Middleware\NotFoundHandler, with an accompanying Zend\Expressive\Container\NotFoundHandlerFactory. This middleware composes and proxies to the NotFoundDelegate detailed in the previous section, and, as such, requires that that service be present.

use Zend\Expressive\Container;
use Zend\Expressive\Delegate;
use Zend\Expressive\Middleware;

return [
    'factories' => [
        Delegate\NotFoundDelegate::class => Container\NotFoundDelegateFactory::class,
        Middleware\NotFoundHandler::class => Container\NotFoundHandlerFactory::class,
    ],
];

When registered, you should then pipe it as the innermost layer of your application:

// A basic application:
$app->pipe(ErrorHandler::class);
$app->pipeRoutingMiddleware();
$app->pipeDispatchMiddleware();
$app->pipe(NotFoundHandler::class);