phproutescakephpmiddleware

Routing Middleware does not preserve headers


I try to handle CORS for the API scoped route. If i get an OPTION request i return the response immediately so the application does not have to handle it. But if i receive a GET request i add the correct headers and return the updated response to the controller (or other middleware if there is any) but then in the controller the response object ($this->response) does not have the previously set headers anymore.

$routes->plugin(
    'Ark',
    ['path' => '/ark'],
    function (RouteBuilder $routes)
    {
        $routes->setRouteClass(DashedRoute::class);
        $routes->registerMiddleware('cors', function($req, $res, $next) {
            $lAllowed = [
                'localhost:8081',
                'localhost:8082',
                'localhost:8087',
                '172.16.1.225',
                '172.16.1.225:8081',
                '172.16.1.225:8082',
                '172.16.1.225:8087',
                '172.16.1.224',
            ];

            if( $sOrigin = $req->getHeader('origin') )
            {
                $sOrigin = reset($sOrigin);
                $sOrigin = str_replace('https://', '', $sOrigin);
                $sOrigin = str_replace('http://', '', $sOrigin);
            }
            if( empty($sOrigin) )
                $sOrigin = $req->host();

            /** @var \Cake\Http\ServerRequest $req */
            /** @var \Cake\Http\Response $res */
            if( in_array($sOrigin, $lAllowed) )
            {
                //debug( 'Allow' );

                $res = $res->cors($req)
                    ->allowOrigin($req->getHeader('origin')) //only one host should be allowed when allow credentials is true
                    ->allowMethods(['GET', 'OPTIONS'])
                    ->allowHeaders(['Content-Type', 'X-CSRF-Token', 'Authorization'])
                    ->allowCredentials()
                    ->maxAge(3600) //1h
                    ->build();


                //return immediately for CORS requests
                if ( strtoupper($req->getMethod()) === 'OPTIONS' )
                {
                    return $res->withStatus(200, 'You shall pass!!');
                }

                //debug( $res );
            }

            return $next($req, $res);
        });


        $routes->prefix('Api', ['path' => '/api'], function(RouteBuilder $route) {
            // Parse specified extensions from URLs
            $route->setExtensions(['json']);

            //allow external services use this api
            $route->applyMiddleware('cors');


            $route->prefix('V1', ['path' => '/v1'], function(RouteBuilder $route) {
                // Translates to `Controller\Api\V1\` namespace

                $lListOfResources = [
                    //Table names...
                ];
                foreach ($lListOfResources as $sResourceName)
                {
                    $route->resources($sResourceName, [
                        'map' => [
                            ':id/restore' => ['action' => 'restore', 'method' => ['PUT', 'PATCH']],
                            'list' => ['action' => 'list', 'method' => 'GET'],
                            'filter/*' => ['action' => 'filter', 'method' => 'GET'],
                        ]
                    ]);
                }

                $route->fallbacks();
            });
        });

        //default landing page
        $routes->connect('/', ['controller' => 'Pages']);
    }
);

Is this a bug or can RoutingMiddleware not modify the response before any controller action? Should i add the CORS logic inside the controllers beforeFilter?

cakephp 4.2.12 php 8.0.1


Solution

  • thanks to user ndm i could fix the problem by modifying the response AFTER the controller was executed

    $routes->plugin(
        'Ark',
        ['path' => '/ark'],
        function (RouteBuilder $routes)
        {
            $routes->setRouteClass(DashedRoute::class);
            $routes->registerMiddleware('cors', function($req, $res, $next) {
                $lAllowed = [
                    'localhost:8081',
                    'localhost:8082',
                    'localhost:8087',
                    '172.16.1.225',
                    '172.16.1.225:8081',
                    '172.16.1.225:8082',
                    '172.16.1.225:8087',
                    '172.16.1.224',
                ];
    
                if( $sOrigin = $req->getHeader('origin') )
                {
                    $sOrigin = reset($sOrigin);
                    $sOrigin = str_replace('https://', '', $sOrigin);
                    $sOrigin = str_replace('http://', '', $sOrigin);
                }
                if( empty($sOrigin) )
                    $sOrigin = $req->host();
    
                /** @var \Cake\Http\ServerRequest $req */
                /** @var \Cake\Http\Response $res */
                if( in_array($sOrigin, $lAllowed) )
                {
    //                debug( 'Allow' );
    
                    //return immediately for CORS requests
                    if ( strtoupper($req->getMethod()) === 'OPTIONS' )
                    {
                        $res = $res->cors($req)
                            ->allowOrigin($req->getHeader('origin')) //only one host should be allowed when allow credentials is true
                            ->allowMethods(['GET', 'OPTIONS'])
                            ->allowHeaders(['Content-Type', 'X-CSRF-Token', 'Authorization'])
                            ->allowCredentials()
                            ->maxAge(3600) //1h
                            ->build();
    
                        return $res->withStatus(200, 'You shall pass!!');
                    }
    
    
                    //run other middleware or controller action, which will generate the final response
                    $res = $next($req, $res);
    
                    //apply CORS headers
                    return $res->cors($req)
                        ->allowOrigin($req->getHeader('origin')) //only one host should be allowed when allow credentials is true
                        ->allowMethods(['GET', 'OPTIONS'])
                        ->allowHeaders(['Content-Type', 'X-CSRF-Token', 'Authorization'])
                        ->allowCredentials()
                        ->maxAge(3600) //1h
                        ->build();
                }
    
                //run default handling, no CORS applied
                return $next($req, $res);
            });
    
    
            $routes->prefix('Api', ['path' => '/api'], function(RouteBuilder $route) {
                // Parse specified extensions from URLs
                $route->setExtensions(['json']);
    
                //allow external services use this api
                $route->applyMiddleware('cors');
    
    
                $route->prefix('V1', ['path' => '/v1'], function(RouteBuilder $route) {
                    // Translates to `Controller\Api\V1\` namespace
    
                    $lListOfResources = [
                        /* TABLES*/
                    ];
                    foreach ($lListOfResources as $sResourceName)
                    {
                        $route->resources($sResourceName, [
                            'map' => [
                                ':id/restore' => ['action' => 'restore', 'method' => ['PUT', 'PATCH']],
                                'list' => ['action' => 'list', 'method' => 'GET'],
                                'filter/*' => ['action' => 'filter', 'method' => 'GET'],
                            ]
                        ]);
                    }
    
                    $route->fallbacks();
                });
            });
    
            //default landing page
            $routes->connect('/', ['controller' => 'Pages']);
    
            $routes->fallbacks();
        }
    );