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/postsAccess-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
| Header | What it allows |
|---|---|
Access-Control-Allow-Origin | Which site may read responses |
Access-Control-Allow-Methods | Which methods may be used |
Access-Control-Allow-Headers | Which request headers may be sent |
Access-Control-Max-Age | For 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.