Setting HTTP status code based on Exception in Slim 4

  • You are here: Free PHP » Uncategorized » Setting HTTP status code based on Exception in Slim 4

One thing that's quite convenient is to be able to throw an exception with a valid HTTP code set and have that code sent to the client.

For example, you may have:

throw new \RuntimeException("Not Found", 404);

With the standard Slim 4 error handler, this response is sent to the client:

$ curl -i -H "Accept:application/json" http://localhost:8888
HTTP/1.1 500 Internal Server Error
Host: localhost:8888
Content-type: application/json

{
    "message": "Slim Application Error"
}

Ideally we want the status code to be 404.

Option 1: Use an HttpException

The simplest solution is to use one of Slim's HttpException classes:

use Slim\Exception\HttpNotFoundException;

...

throw new HttpNotFoundException($request);

This is only useful in a Request Handler as you need a Request object, but the expected response is sent to the client:

$ curl -i -H "Accept:application/json" http://localhost:8888
HTTP/1.1 404 Not Found
Host: localhost:8888
Content-type: application/json

{
    "message": "404 Not Found"
}

Simple and easy!

Option 2: Override the ErrorMiddleware

There are situation when you can't simply replace the exception thrown. For example, you're updating an application from Slim 3 and you have hundreds of customised exceptions already throwing, or you throw from a class that doesn't have a Request object instance available.

In these cases, the easiest solution is to extend the Slim ErrorMiddleware to wrap the exception in an

HttpException

and then the standard error handling and rendering will "just work".

I'm feeling a little lazy, so let's use an anonymous class to do replace the call to $app->addErrorMiddleware():

$container = $app->getContainer();
    $logger = $container->has(LoggerInterface::class) ?$container->get(LoggerInterface::class) : null;

    $errorMiddleware = new class (
        callableResolver: $app->getCallableResolver(),
        responseFactory: $app->getResponseFactory(),
        displayErrorDetails: false,
        logErrors: true,
        logErrorDetails: true,
        logger: $logger
    ) extends \Slim\Middleware\ErrorMiddleware {
        public function handleException(
            ServerRequestInterface $request,
            Throwable $exception
        ): \Psr\Http\Message\ResponseInterface
        {
            // determine that this exception should be wrapped. I'm checking for code between 400 & 599
            if ($exception->getCode() >= 400 && $exception->getCode() < 600) {
                // wrap the exception in an HttpException
                $exception = new \Slim\Exception\HttpException(
                    $request,
                    $exception->getMessage(),
                    $exception->getCode(),
                    $exception
                );
                $exception->setTitle($exception->getMessage());
            }
            return parent::handleException($request, $exception);
        }
    };
    $app->addMiddleware($errorMiddleware);

Behind the scenes of $app->addErrorMiddleware(), the \Slim\Middleware\ErrorMiddleware is constructed and then added to the middleware stack. We replicate that we an anonymous class that overrides handleException() to wrap the thrown exception if required.

Looking at the code in detail

There's quite a lot going on here, so let's break it down into parts.

$container = $app->getContainer();
    $logger = $container->has(LoggerInterface::class) ?$container->get(LoggerInterface::class) : null;

    $errorMiddleware = new class (
        callableResolver: $app->getCallableResolver(),
        responseFactory: $app->getResponseFactory(),
        displayErrorDetails: false,
        logErrors: true,
        logErrorDetails: true,
        logger: $logger
    ) extends \Slim\Middleware\ErrorMiddleware {

The constructor to \Slim\Middleware\ErrorMiddleware takes 6 parameters, so when we instantiate, we have to pass them all in though it's not unusual for the $logger parameter to be left off in the call to $app->addErrorMiddleware(). The easiest way to get a logger instance if there is one, is to grab it from the container where it should be registered under the \Psr\Log\LoggerInterface key which is imported into the file with a use statement.

I've used PHP 8's named arguments as this constructor takes three booleans and it's easier to remember what they do if they are label

Truncated by Planet PHP, read more at the original (another 2865 bytes)

Powered by Gewgley