Implementing CORS in Zend Expressive
On a recent project, I needed to implement CORS support for my Expressive API. The easiest way to do this is to use Mike Tuupola's PSR-7 CORS Middleware.
As this is a standard Slim-Style PSR-7 middleware implementation, we need to wrap it for Expressive, so we make a factory:
App/Factory/CorsMiddlewareFactory.php:
<?php declare(strict_types=1); namespace App\Factory;
use Tuupola\Middleware\Cors; use Zend\Diactoros\Response; use Zend\Stratigility\Middleware\CallableMiddlewareWrapper;
class CorsMiddlewareFactory { public function __invoke($container) { return new CallableMiddlewareWrapper( new Cors([ "origin" => ["*"], "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"], "headers.allow" => ["Content-Type", "Accept"], "headers.expose" => [], "credentials" => false, "cache" => 0, ]), new Response() ); } }
We then register this in our App\ConfigProvider::getDependencies() by adding to the factories key so that it looks something like this:
'factories' => [ Action\HomePageAction::class => Action\HomePageFactory::class, \Tuupola\Middleware\Cors::class => Factory\CorsMiddlewareFactory::class, ],
If you don't want to fully qualify, add use Tuupola\Middleware\Cors; to the top of the file so that you can just use Cors::class here.
Lastly, we register the middleware in config/pipeline.php:
$app->pipe(\Tuupola\Middleware\Cors::class);
Place it somewhere near the top of the list; personally, I place it just after piping the ServerUrlMiddleware::class.
We now have working CORS:
$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Accept, Content-Type" \ -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/ HTTP/1.1 200 OK Host: localhost:8890 Connection: close Access-Control-Allow-Origin: http://localhost Vary: Origin Access-Control-Allow-Headers: content-type, accept Content-type: text/html; charset=UTF-8
Failure looks like this:
$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Accept, Content-Type X-Clacks-Overhead" \ -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/ HTTP/1.1 401 Unauthorized Host: localhost:8890 Connection: close Content-type: text/html; charset=UTF-8
By default, it doesn't tell you what went wrong, which isn't too helpful.
Providing JSON error responses
To provide a JSON error response, you need to set the error option to a callable and you can then return a JsonResponse:
App/Factory/CorsMiddleware.php:
<?php declare(strict_types=1); namespace App\Factory;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Tuupola\Middleware\Cors; use Zend\Diactoros\Response; use Zend\Diactoros\Response\JsonResponse; use Zend\Stratigility\Middleware\CallableMiddlewareWrapper;
class CorsMiddlewareFactory { public function __invoke($container) { return new CallableMiddlewareWrapper( new Cors([ "origin" => ["*"], "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"], "headers.allow" => ["Content-Type", "Accept"], "headers.expose" => [], "credentials" => false, "cache" => 0, "error" => [$this, 'error'], ]), new Response() ); }
public static function error( RequestInterface $request, ResponseInterface $response, $arguments) {
return new JsonResponse($arguments); } }
As you can see, we've created a new error method that returns a JsonResponse. We can then encode the $arguments as that contains the information about what the problem is:
$ curl -X OPTIONS -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Accept, Content-Type X-Clacks-Overhead" \ -H "Origin: http://localhost" -H "Accept: application/json" http://localhost:8890/ HTTP/1.1 401 Unauthorized Host: localhost:8890 Date: Sat, 21 Oct 2017 10:41:18 +0000 Connection: close X-Powered-By: PHP/7.1.8 Content-Type: application/json
{"message":"CORS requested h
Truncated by Planet PHP, read more at the original (another 1594 bytes)