Multiple Middleware: one only executed AFTER app

Hi, in my Slim 4 app I use middleware on all routes (e.g. Routing Middleware) which is set last (therefore executed first) and some middlewares on single routes. It seems the app code is executed before the second middleware (the ‘SlimMiddlewareRequestHeadersCheck’), indicated by an error thrown by Guzzle before the middleware should return a 403. If I delete the relevant code for Guzzle a 403 is thrown, indicatin to me, the second middleware runs after the app code. Why and how can I have it run before?

$app->get('/test', function (Request $request, Response $response, $args) {

    $client = new GuzzleClient(['base_uri' => BASE_URI]);

    $allRequestHeadersTransformed = array();
    $allRequestHeadersTransformed['X-Requested-With'] = $request->getHeader('X-Requested-With'); 

   $options = [
         'headers' => $allRequestHeadersTransformed,
         'http_errors' => false
   ];

   $responseFromBackendApi = $client->request($request->getMethod(),
        'v1/testroute',
        $options
   );
...
...
...

    $response = $response->withStatus($status);

    return $response;
})
    ->add(new SlimMiddlewareRequestHeadersCheck())
    ->add(new SlimMiddlewareCorsAndOriginCheck());

Middleware:

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;


final class SlimMiddlewareRequestHeadersCheck {

    public function __invoke(Request $request, RequestHandler $handler) : Response {

        $response = $handler->handle($request);

...

        if($xRequestedWithHeader === null || $xRequestedWithHeader != 'xmlhttprequest') {

            $response = new \Slim\Psr7\Response();
            $response = $response->withStatus(403); //forbidden

        } 
...

        return $response;

    }

}

IIRC you need to rearrange your middleware like this:

Thanks. But isn’t it the same as my code (I have the $reponse = $handler->handle($request) at the top and if the If-Block is not executed this $response is returned)?

Maybe I misunderstand how middleware works. I assume if Middleware BEFORE route closure is excuted and returns a new response with error code, the route closure is not even touched or is?

Your original query was:

It seems the app code is executed before the second middleware

Calling $handler->handle($request) before your code will run the rest of the middleware stack and the route handler (think of the route handler as at the end of the middleware stack).

So if you want your code to run first you need to place it before this statement. If you want your code to run after, place your code after it.

1 Like

I am bit confused now. I thought the onion layer structure means this: middleware1 → middleware2 → app → middleware2 → middleware1
And if any middleware creates a new Response with 4xx status code and returns it, the next middleware and app are not reached.


// ingoing middleware
// ...

$response = $handler->handle($request);`

// outgoing middleware
// ...

Everything before this line is executed before the last middleware (route dispatcher) calls the action handler. Ingoing middleware.

Everything after this line is being executed after the last middleware (route dispatcher) has called your action handler. Outgoing middleware.

There can also be a mix of both types.

The middleware order in Slim is LIFO, so the last middleware will be executed first. In the most cases this is the Slim ErrorHandler middleware.

And if any middleware creates a new Response with 4xx status code and returns it, the next middleware and app are not reached.

This is correct. When you don’t invoke the handlers handle method you have to create and return your own Response object. Then only the outgoing middlewares will handle this response object.

1 Like

Great! Finally understood.

I did not manage to get it working without checking for an error status code from a middleware executed before (return $response->withStatus(403) does not stop the execution of next middleware). What am I doing wrong?

Route:

$app->get('/test', function (Request $request, Response $response, $args) {

    $client = new GuzzleClient(['base_uri' => BASE_URI]);

    $allRequestHeadersTransformed = array();
    $allRequestHeadersTransformed['X-Requested-With'] = $request->getHeader('X-Requested-With'); 

   $options = [
         'headers' => $allRequestHeadersTransformed,
         'http_errors' => false
   ];

   $responseFromBackendApi = $client->request($request->getMethod(),
        'v1/testroute',
        $options
   );
...
...
...

    $response = $response->withStatus($status);

    return $response;
})
    ->add(new SlimMiddlewareRequestHeadersCheck())
    ->add(new SlimMiddlewareCorsAndOriginCheck());

SlimMiddlewareRequestHeadersCheck:

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;


final class SlimMiddlewareRequestHeadersCheck {

    public function __invoke(Request $request, RequestHandler $handler) : Response {

        $givenHeadersNew = array_change_key_case($request->getHeaders(),CASE_LOWER);

        if(array_key_exists("x-requested-with",$givenHeadersNew)) {

            $xRequestedWithHeader = strtolower($givenHeadersNew["x-requested-with"][0]);

        } else {

            $xRequestedWithHeader = null;

        }

        if($xRequestedWithHeader === null || $xRequestedWithHeader != 'xmlhttprequest') {

            $response = new \Slim\Psr7\Response();
            $response = $response->withStatus(403);
            return $response;

        } else {

            return $handler->handle($request);

        }

    }

}

SlimMiddlewareCorsAndOriginCheck:

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext;

final class SlimMiddlewareCorsAndOriginCheck {

    public function __construct(protected bool $auth = true, protected bool $browserShouldCachePreflight = true){
    }

public function __invoke(Request $request, RequestHandlerInterface $handler) : Response {

        if($something) {
            ...

            if(isset($originIsAllowed)) { 

                $before = $handler->handle($request);
                $existingContent = (string) $before->getBody();
                $existingHeaders = (array) $before->getHeaders();
                $status = (int) $before->getStatusCode();
                
                // ??? WHY DO I NEED THIS IF-BLOCK ??? 
                if ($status > 399) {
                    return $handler->handle($request);
                }

                $response = new \Slim\Psr7\Response();
                foreach($existingHeaders as $key=>$value) {
                    $response = $response->withHeader($key, $value);
                }
                $response->getBody()->write($existingContent);

                $response = $response->withHeader('Access-Control-Allow-Origin', $originIsAllowed);
                $response = $response->withHeader('Vary', 'origin'); 

                if($somethingElse) {
                
                // SEVERAL NESTED IF-ELSE-BLOCKS WITH EITHER: $response = new \Slim\Psr7\Response() and return with error status OR: $response = $response->withHeader('newHeader', 'value');
                } else { 

                    $response = $response->withHeader('Access-Control-Expose-Headers', ''); 
                    return $response;
                }

            } else { // origin not allowed, 403 forbidden

                $response = new \Slim\Psr7\Response();
                $response = $response->withStatus(403);
                return $response;

            }

        } elseif(!$something && $anotherVar) { 

            $response = new \Slim\Psr7\Response();
            $response = $response->withStatus(400);
            return $response;

        } else { 

            return $handler->handle($request);

        }

       return $response; // not used because return in each case before

    }

}

Returning another response object does not stop the middleware stack. The outgoing middlewares will be processed anyway.

To stop the execution, you could throw an Exception instead. The Slim ErrorMiddleware is then able to catch and render it into an HTML or JSON response.

For the status code 403 you could throw a HttpForbiddenException.

Replace this:

$response = new \Slim\Psr7\Response();
$response = $response->withStatus(403);

with this:

throw new \Slim\Exception\HttpForbiddenException();

There are even more HTTP specific Exception classes:

400 = \Slim\Exception\HttpBadRequestException
401 = \Slim\Exception\HttpUnauthorizedException
404 = \Slim\Exception\HttpNotFoundException
405 = \Slim\Exception\HttpMethodNotAllowedException
500 = \Slim\Exception\HttpInternalServerErrorException
501 = \Slim\Exception\HttpNotImplementedException
1 Like

It seems to work. Thank you!! Finding this in the documentation on middleware would be a big plus, I guess.

How would I throw an exception if no class is available like for 409 (conflict). Do I need to extend HttpSpecializedException ?

Yes, just extend from it to create your own errors.

1 Like

I recognized that Slim always returns 200 for an OPTIONS request, independent of any thrown exception. Is there a recommended way on dealing with missing or not-allowed origins in a preflight request? I would throw 400 and 403 respectively (but the 200 is returned as mentioned). Mozilla (Access-Control-Allow-Origin - HTTP | MDN) says nothing about it and for security reasons recommends to avoid “null” for the Access-Control-Allow-Origin (ACAO) header. Best idea so far: send response to preflight without the ACAO header when origin is missinn or not allowed

Yes, but this should only be the case when the lazy CORS route ($app->options('/{routes:.+}' is enabled. Is this how it is implemented in your application?

I implemented a few single OPTIONS routes and a catch-all one for any other route, like:

 $app->options('/{routes:' . $regexToExcludeFromAllRoutes . '.+}', return $response; })->add(new SlimMiddlewareCorsAndOriginCheck();

The $regexToExcludeFromAllRoutes includes the routes for which I already have an OPTIONS method implemented. The catch-all OPTIONS route is the second last in my code, just before a catch-all any: any(’/routes:.+}’

On both implementations (single and catch-all OPTIONS route) a 200 is returned for missing or not-allowed origin and the error (400 or 403) is sent in body with some error message