Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

CORS

A browser does not let JavaScript read responses from another site: a script on the app.example.com page gets no data from api.example.com until the API itself allows it. The permission is given through response headers — this mechanism is called CORS. Requests from curl, mobile apps and other servers are not restricted.

What the problem looks like

If a page on another site calls /api/posts, the request reaches Slim and gets a response, but the browser does not hand it to the script, and the console shows a message about being blocked by the CORS policy. The fix is on the API side.

The middleware

// src/Middleware/CorsMiddleware.php
namespace App\Middleware;
 
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
 
final class CorsMiddleware implements MiddlewareInterface
{
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        return $handler->handle($request)
            ->withHeader('Access-Control-Allow-Origin', 'https://app.example.com');
    }
}

The middleware adds a header to the finished response. It names the site that may read responses — with the scheme, without a path or a trailing slash.

Attaching it

Only the API needs the headers, so the middleware is attached to its group:

// config/routes.php
$app->group('/api', function (RouteCollectorProxy $api) {
    $api->group('/posts', function (RouteCollectorProxy $group) {
        // the post routes from earlier lessons
    });
})->add(CorsMiddleware::class);

Check from the terminal:

curl -i http://localhost:8080/api/posts

Access-Control-Allow-Origin appears among the response headers.

The preflight request

Before a request with the POST, PUT or DELETE method, with JSON or with an Authorization header, the browser first sends an OPTIONS request by itself — it asks what is allowed. The answer must list the methods and headers. Extend the middleware:

// src/Middleware/CorsMiddleware.php
return $handler->handle($request)
    ->withHeader('Access-Control-Allow-Origin', 'https://app.example.com')
    ->withHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
    ->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

Group middleware runs only when a route was found for the request. The API has no OPTIONS routes, so add one catch-all route to the /api group:

// config/routes.php
$api->options('/{routes:.+}', function (Request $request, Response $response) {
    return $response;
});

The .+ pattern matches any address inside the group. The handler returns an empty response, and CorsMiddleware adds the headers to it.

CORS headers

HeaderWhat it allows
Access-Control-Allow-OriginWhich site may read responses
Access-Control-Allow-MethodsWhich methods may be used
Access-Control-Allow-HeadersWhich request headers may be sent
Access-Control-Max-AgeFor how many seconds the browser remembers the permission

Several sites

The header can hold only one address. If several sites are allowed, the list is kept in the settings, and the middleware compares the request's Origin header with it and puts the matching address into the response.

Common mistake

The value * in Access-Control-Allow-Origin lets any site read. For an open read-only API that is acceptable, but if the API is closed with a token, list specific addresses. And remember: CORS is not API protection. It restricts only scripts in a browser; access is closed by the token check.

Thanks for reading!

Donate to the site — we work hard for you.