ToX 82
ToX 82

Reputation: 1074

CORS errors because of internal app error

I am creating a simple API with CakePHP 4, and I am having some issues with some CORS requests.

Access to XMLHttpRequest at 'http://localhost/myapp.api/elaborations/add.json' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Everything works with every other request and after some digging I've found that the error was an undefined index in my controller. If I fix that, the CORS error disappears. I just didn't see it in my log files, it's my bad.

It's a bit confusing seeing a CORS error because of a coding error though. I guess the issue could be in my CORS configuration, and hence this question.

This is what I've ended with, after a little bit of web search, trials and errors. I know it's ugly but I couldn't find anything better that actually worked.

How can I avoid having CORS errors for coding issues? I guess there is some redirect action somewhere, but I can't figure out how to avoid it.

<?php
namespace App\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Server\MiddlewareInterface;

class CorsMiddleware implements MiddlewareInterface
{
    public function process(
        ServerRequestInterface $request,
        RequestHandlerInterface $handler
    ): ResponseInterface
    {
        // Calling $handler->handle() delegates control to the *next* middleware
        // In your application's queue.
        $response = $handler->handle($request);

        if ($request->getHeader('Origin')) {
            $allowedDomains = [
                'https://myapp.it',
                'https://www.myapp.it',
                'http://localhost:3000',
            ];
            $origin = $_SERVER['HTTP_ORIGIN'];

            if (in_array($origin, $allowedDomains)) {
                header('Access-Control-Allow-Origin: ' . $origin);
            }

            header('Access-Control-Allow-Methods: POST, GET, PUT, PATCH, DELETE, OPTIONS');
            header('Access-Control-Allow-Headers: *');

            if (strtoupper($request->getMethod()) === 'OPTIONS') {
                exit(0);
            }
        }

        return $response;
    }
}

Upvotes: 0

Views: 1436

Answers (1)

ndm
ndm

Reputation: 60473

First of all, do not access superglobals in CakePHP directly, always use the abstracted APIs! Also you shouldn't echo data manually, that includes headers, again, use the abstracted APIs! Using superglobals and echoing data will only get you in trouble, it messes up the testing environment, it can lead to data not being sent (properly), etc.

That being said, for proper CORS coverage you also need to modify error handling, as the error controller will create a new response instance. You should be able to process the request/response in a custom exception renderer, at ExceptionRenderer::_getController(), like so:

// in src/Error/AppExceptionRenderer.php
namespace App\Error;

use App\Http\Middleware\CorsMiddleware;
use Cake\Controller\Controller;
use Cake\Error\ExceptionRenderer;

class AppExceptionRenderer extends ExceptionRenderer
{
    protected function _getController(): Controller
    {
        $controller = parent::_getController();

        $cors = new CorsMiddleware();
        $response = $cors->setHeaders(
            $controller->getRequest(),
            $controller->getResponse()
        );

        return $controller->setResponse($response);
    }
}
// in config/app.php
'Error' => [
    'exceptionRenderer' => \App\Error\AppExceptionRenderer::class,
    // ...
],

Your CORS middleware would supply the setHeaders() method accordingly, where you set the CORS headers if required, based on your example it could look something like this:

public function process(
    ServerRequestInterface $request,
    RequestHandlerInterface $handler
): ResponseInterface
{
    $response = $handler->handle($request);
    $response = $this->setHeaders($request, $response);

    return $response;
}


public function setHeaders(
    ServerRequestInterface $request,
    ResponseInterface $response
): ResponseInterface
{
    if ($request->getHeader('Origin')) {
        $allowedDomains = [
            'https://myapp.it',
            'https://www.myapp.it',
            'http://localhost:3000',
        ];
        
        $origins = $request->getHeader('Origin');
        $lastOrigin = end($origins);
        if (in_array($lastOrigin, $allowedDomains, true)) {
            $response = $response
                ->withHeader('Access-Control-Allow-Origin', $lastOrigin);
        }

        if (strtoupper($request->getMethod()) === 'OPTIONS') {
            $response = $response
                ->withHeader(
                    'Access-Control-Allow-Methods',
                    'POST, GET, PUT, PATCH, DELETE, OPTIONS'
                )
                ->withHeader('Access-Control-Allow-Headers', '*');
        }
    }

    return $response;
}

See also

Upvotes: 2

Related Questions