Before fetch sends a POST request to another domain it will do a CORS pre-flight check. The following website has the HTTP OPTIONS middleware code you can use for CakePHP 3. It mentions for a CakePHP 2 version you need to go to another website which is unavailable now (July 2019). See below for my CakePHP 2 code do do the same thing.

CakePHP 2.x DispatchFilter CORS ‘OPTIONS’ pre-flight check response code

This basically says if the request is an OPTIONS check is from the right origin and the requested method is allowed respond with the right headers.

<?php

/**
 * CakePHP 2 DispatchFilter code for CORS OPTIONS pre-flight check
 * fetch sends an OPTIONS pre-flight check
 * copy this file as OptionsFilter.php to app/Routing/Filter/
 * load this DispatcherFilter by finding this block of code in
 * app/Config/bootstrap.php and appending the
 * 'OptionsFilter' entry
 * Configure::write('Dispatcher.filters', [
 *    'AssetDispatcher',
 *    'CacheDispatcher',
 *    'OptionsFilter' // custom by tgn
 * ]);
 *
 */

App::uses('DispatcherFilter', 'Routing');
App::uses('Configure', 'Core');

class OptionsFilter extends DispatcherFilter
{

    /**
     * @var int
     */
    public $priority = 9;

    /**
     * @param CakeEvent $event
     * @return mixed
     */
    public function beforeDispatch(CakeEvent $event)
    {
        $request = $event->data['request'];
        $response = $event->data['response'];

        // here is your allowed origins
        // I set them using Configure in bootstrap.php
        // Configure::write('ALLOWED_METHODS', ['PUT', 'POST', 'DELETE']);
        // Configure::write('ALLOWED_ORIGINS',['http://localhost:3000', 'http://localhost:8081']);

        $allowedOrigins = Configure::read('ALLOWED_ORIGINS');
        // here are the allowed methods
        $allowedMethods = Configure::read('ALLOWED_METHODS');

        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
        // 2 hours
        $accessControlMaxAge = '7200';

        if ($request->is('OPTIONS')) {

            // where is the request coming from
            // e.g. from my react dev environment
            // it is http://localhost:3000
            $origin = $request->header('Origin');

            // what method are they requesting to use
            $method = $request->header('Access-Control-Request-Method');

            // what headers are they sending
            $headers = $request->header('Access-Control-Request-Headers');

            // if allow then set headers
            if (in_array($origin, $allowedOrigins)) {
                $response->header(
                    'Access-Control-Allow-Origin',
                    $origin
                );
            }

            if (in_array($method, $allowedMethods)) {
                $response->header(
                    "Access-Control-Allow-Methods", $method
                );
            }

            $response->header(
                "Access-Control-Allow-Headers",
                $headers
            );
            $response->header('Access-Control-Allow-Credentials', "true");
            $response->header('Access-Control-Max-Age', $accessControlMaxAge);
            $response->header('Content-Type', 'application/json');

            // this is just me goofing off
            // but this response viewed in the dev tools of the
            // browsers lets me know
            // I have triggered the DispatchFilter
            $response->body(
                json_encode(
                    [
                        'options' => 'pre-flight from dispatcher'
                    ]
                )
            );
            $event->stopPropagation();
            return $response;
        }

    }
}

Code to put in CakePHP 2.x Actions to allow POST

  public function add($shipment_type = null)
    {

        $last = null;
        $error = null;

        $origin = $this->request->header('Origin');
        $allowedOrigins = Configure::read('ALLOWED_ORIGINS');
        if (in_array($origin, $allowedOrigins)) {
            $this->response->header('Access-Control-Allow-Origin', $origin);
        }
        // rest of your action code

Chrome DevTools View of the OPTIONS pre-flight

Response from the CakePHP 2.x DispatcherFilter code above

Note the OPTIONS Request Headers are Access-Control-Request-Headers: content-type, x-requested-with, Access-Control-Request-Method: POST and Origin: http://localhost:3000

The dispatch filter sends the following headers as response

  1. Access-Control-Allow-Credentials: true
  2. Access-Control-Allow-Headers: content-type,x-requested-with
  3. Access-Control-Allow-Methods: POST
  4. Access-Control-Allow-Origin: http://localhost:3000
  5. Access-Control-Max-Age: 7200

You may have seen ‘Access-Control-Allow-Origin’ set to ‘*’

POST request after receiving the correct response headers

This is the fetch code in a react App that generates the OPTIONS and POST request.

fetch(url, {
	method: "POST", // *GET, POST, PUT, DELETE, etc.
	mode: "cors", // no-cors, cors, *same-origin
	cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
	credentials: "same-origin", // include, *same-origin, omit
	headers: {
		"Content-Type": "application/json",
		Accept: "application/json",
		"X-Requested-With": "XMLHttpRequest"
	},
	redirect: "error", // manual, *follow, error
	//referrer: "no-referrer", // no-referrer, *client
	body: JSON.stringify(postObject) // body data type must match "Content-Type" header
}).then(response => response.json());

CakePHP 3 Options Middleware Code

Just in case the original website goes away

<?php

namespace App\Middleware;

class HttpOptionsMiddleware
{
    public function __invoke($request, $response, $next)
    {
        $response = $response->withHeader('Access-Control-Allow-Origin', '*');

        if ($request->getMethod() == 'OPTIONS')
        {
            $method = $request->getHeader('Access-Control-Request-Method');
            $headers = $request->getHeader('Access-Control-Request-Headers');
            $allowed = empty($method) ? 'GET, POST, PUT, DELETE' : $method;

            $response = $response
                            ->withHeader('Access-Control-Allow-Headers', $headers)
                            ->withHeader('Access-Control-Allow-Methods', $allowed)
                            ->withHeader('Access-Control-Allow-Credentials', 'true')
                            ->withHeader('Access-Control-Max-Age', '86400');

            return $response;
        }

        return $next($request, $response);
    }
}

Register Middleware in src/Application for CakePHP 3.x

To install the CakePHP 3 httpOptionsMiddleware

mkdir src/Middleware

create a new file named src/Middleware/HttpOptionsMiddleware.php and put the contents of the below code into it

Open src/Application.php include a use statement for the code and append the new hook code for the middleware:

<?php
use App\Middleware\HttpOptionsMiddleware;

class Application extends BaseApplication {
 public function middleware($middlewareQueue)
    {
        $middlewareQueue
        // Add routing middleware.
        ->add(new RoutingMiddleware($this))
        //... other middleware etc
        // add http options
        ->add( new HttpOptionsMiddleware($this));
        return $middlewareQueue;
     }