CORS — Cross-Origin Resource Sharing
Also Known As
TL;DR
Explanation
The Same-Origin Policy prevents JavaScript from making requests to a different domain, protocol, or port than the page it is running on. CORS is the controlled mechanism for relaxing this restriction. For simple requests (GET, POST with certain content types), the browser adds an Origin header; the server responds with Access-Control-Allow-Origin and the browser either allows or blocks the response. For complex requests (PUT, DELETE, custom headers, JSON content-type), the browser sends a preflight OPTIONS request first to check permissions; the server must respond with Access-Control-Allow-Methods, Access-Control-Allow-Headers, and optionally Access-Control-Max-Age. In PHP APIs, CORS headers must be set on every response including error responses. Wildcard (*) for Access-Control-Allow-Origin is only appropriate for public APIs — for APIs that use cookies or Authorization headers, the specific origin must be listed and Access-Control-Allow-Credentials: true must be set.
Common Misconception
Why It Matters
Common Mistakes
- Not handling OPTIONS preflight requests — complex requests get a preflight that must return 200 with CORS headers, not a 405 Method Not Allowed.
- Setting CORS headers only on success responses — error responses (4xx, 5xx) must also include CORS headers or the browser hides the error from the JavaScript.
- Using Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers reject this combination; specify the exact origin.
- Setting CORS headers in application middleware only — some PHP frameworks handle OPTIONS at the router level before middleware runs; handle it earlier.
Code Examples
// Missing preflight handling — CORS errors on all complex requests
header('Access-Control-Allow-Origin: *');
// No OPTIONS handling — preflight fails with 404/405
// With credentials — wildcard + credentials = browser rejection
// Handle preflight + correct headers
function setCorsHeaders(string $allowedOrigin): void {
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin === $allowedOrigin) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin'); // important for caching
}
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-Token');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400'); // cache preflight 24hrs
}
// Handle preflight before routing
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
setCorsHeaders('https://app.example.com');
http_response_code(200);
exit;
}