Reader Stacks

How to Enable CORS in a Laravel or PHP API

A browser blocks cross-origin requests by default — CORS headers on the server are what explicitly permit a specific frontend origin to actually call your API.

CORS (Cross-Origin Resource Sharing) errors happen when a browser blocks a JavaScript request from one origin (say, app.example.com) to an API on a different origin (api.example.com) — this is a genuine browser security restriction, and the fix lives entirely on the server side: sending headers that explicitly permit the calling origin.

Laravel's built-in CORS configuration

// config/cors.php
return [
    'paths' => ['api/*'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['https://app.example.com'],
    'allowed_headers' => ['*'],
    'supports_credentials' => false,
];

Modern Laravel includes CORS handling in the framework, so the actual work is usually configuration rather than writing headers by hand. On current Laravel applications config/cors.php may not be published by default; if it is missing, publish the CORS configuration first and then set the allowed origins, methods, headers, and credential behavior your frontend actually needs.

Why allowed_origins shouldn't just be a wildcard in production

'allowed_origins' => ['*'], // permissive, generally not appropriate for a production API with credentials

A wildcard origin is convenient for local development, but for a production API — especially one using cookies or authentication headers — listing the actual specific frontend origin(s) is the safer choice, and is in fact required by the CORS specification itself once supports_credentials is true (browsers reject a wildcard origin combined with credentialed requests outright).

Enabling credentialed requests (cookies, Authorization headers)

'supports_credentials' => true,
// on the frontend, fetch() needs this too
fetch('https://api.example.com/user', {
    credentials: 'include',
})

Both sides need to agree — the server's CORS config allowing credentials, and the frontend's request explicitly including them — for cookie-based cross-origin authentication to work at all.

Handling CORS manually in plain PHP (without Laravel)

<?php
header('Access-Control-Allow-Origin: https://app.example.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

// actual API logic continues below

Handling the OPTIONS method explicitly is necessary because browsers send a "preflight" OPTIONS request before certain cross-origin requests (like a POST with a JSON content type), checking the CORS headers before sending the real request — without responding successfully to this preflight, the browser never sends the actual request at all.

Debugging a CORS error

The browser's console error message for a CORS failure names the specific missing or mismatched header — checking that error message against the server's actual response headers (via browser dev tools' Network tab, not just the request) is the fastest way to identify exactly which part of the CORS configuration is missing or incorrect.

Publishing the Laravel CORS config when it is missing

php artisan config:publish cors

After publishing, clear or rebuild cached configuration in the environment where the change is deployed. Editing config/cors.php while the application is still serving an older cached config produces the same confusing symptom as many other Laravel config changes: the file looks right but the response headers do not change.

Not every cross-origin request triggers a preflight

Browsers can send some "simple" cross-origin requests directly and validate the response's CORS headers afterward. Requests using methods or headers outside that simple set — JSON requests commonly do — trigger an OPTIONS preflight first. This distinction explains why one endpoint can appear to work while another endpoint to the same host fails before the application receives the real request.

CORS is not authentication or CSRF protection

CORS controls what browser JavaScript is allowed to read across origins. It does not stop curl, server-to-server clients, mobile apps, or an attacker from sending requests to a public endpoint. Protect sensitive API actions with authentication and authorization independently. For cookie-authenticated SPAs, CSRF and cookie attributes also matter; a correct Access-Control-Allow-Origin header does not make a cross-site cookie setup safe by itself.

Watch for redirects on preflight requests

A common failure is that OPTIONS /api/... gets redirected to a login page, an HTTPS canonicalization layer, or a trailing-slash URL before CORS headers are added. In the Network panel, inspect the preflight response itself, including its status and headers. Fix the request path or middleware order so the preflight receives the intended CORS response instead of trying to add more headers to the frontend request.