GuidesJVM & PHP

Fix CORS errors in Laravel

A Laravel CORS error means the browser withheld a cross-origin response because the required access headers were missing or mismatched. In Laravel these headers come from one place: the built-in HandleCors middleware, configured through config/cors.php.

Confirm the failure is a CORS error

Open your browser developer tools and verify the failure type before altering application files. Follow the diagnosis guide to distinguish missing CORS headers from network-level failures.

The Network tab shows whether an OPTIONS preflight failed or the actual response lacked Access-Control-Allow-Origin. When a preflight fails with a 4xx or 5xx status, the browser cancels the real request entirely.

Configure the built-in HandleCors middleware

CORS support has been built into Laravel since version 9 through the Illuminate\Http\Middleware\HandleCors middleware, which sits in the default global middleware stack and answers OPTIONS preflights automatically. On Laravel 11 and newer the middleware stack is configured in bootstrap/app.php; on Laravel 10 and earlier it lives in the $middleware array of app/Http/Kernel.php. Do not install the abandoned fruitcake/laravel-cors package on modern Laravel.

Laravel 11 and newer ship a slim skeleton without a config/cors.php file. Run php artisan config:publish cors to create it; on Laravel 10 and earlier the file is part of the default config directory. The published defaults are paths set to ['api/*', 'sanctum/csrf-cookie'], allowed_methods set to ['*'], allowed_origins set to ['*'], allowed_origins_patterns set to [], allowed_headers set to ['*'], exposed_headers set to [], max_age set to 0, and supports_credentials set to false.

config/cors.php after php artisan config:publish cors
return [

    // Only requests matching these paths get CORS headers
    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    'allowed_methods' => ['*'],

    // Replace the wildcard with your front-end origin in production
    'allowed_origins' => ['https://app.example.com'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => false,

];

Restrict origins for production

The HandleCors middleware only adds headers to requests matching the patterns in paths. Routes outside /api receive no CORS headers by default, so add path patterns if your front end calls routes outside that prefix.

Replace the wildcard in allowed_origins with explicit https origins for any API that serves private data. The wildcard is acceptable only for truly public endpoints that never involve credentials.

How preflights are handled

The middleware treats a request as a preflight when it uses the OPTIONS method and carries both Origin and Access-Control-Request-Method headers. It then returns the response directly, without executing route logic, using the values from allowed_methods, allowed_headers, and max_age. Setting allowed_headers to ['*'] reflects the headers the client requested.

When a preflight fails in a Laravel app, check three common causes.

  • The requested URI does not match any entry in the paths configuration array.
  • The configuration is cached, so changes to config/cors.php are ignored until you run php artisan config:clear or recompile with php artisan config:cache.
  • A web server or load balancer in front of Laravel answers OPTIONS requests before PHP receives the traffic.

Send cookies without hitting the wildcard trap

Browsers reject Access-Control-Allow-Origin: * when the request carries credentials such as cookies. Setting 'supports_credentials' => true therefore requires explicit entries in allowed_origins, such as ['https://app.example.com']; the wildcard stops working. This pairing is what Laravel Sanctum single-page applications rely on for session cookies, which is why the default paths include sanctum/csrf-cookie.

Cookies across origins: both sides must opt in
// config/cors.php: wildcards stop working once credentials are on
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => ['https://app.example.com'],
'supports_credentials' => true,

// The browser attaches cookies only when the client asks for them
const response = await fetch('https://api.example.com/api/user', {
  credentials: 'include',
});

The client must opt in by passing credentials: 'include' to fetch. Credentials are session secrets, so never route them through a public proxy; configure your Laravel application directly for authenticated cross-origin requests.

Verify the headers with curl

An OPTIONS probe sends the Origin, Access-Control-Request-Method, and Access-Control-Request-Headers headers to trigger the middleware, and expects access-control-allow-origin plus the allowed methods and headers in return. A plain GET probe confirms the actual response carries the origin header.

Probe the preflight and the actual request
# Preflight probe
curl -i -X OPTIONS http://localhost:8000/api/items \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type"

# Expect access-control-allow-origin plus the allowed methods and headers

# Actual request probe
curl -i http://localhost:8000/api/items \
  -H "Origin: https://app.example.com"

# Missing the header? Check paths in config/cors.php, then clear
# the config cache: php artisan config:clear

curl ignores CORS and displays responses even when headers are absent. The probes exist to inspect what the server emits; see the Postman comparison for why direct clients bypass browser checks.

Good questions.

Is the fruitcake/laravel-cors package still needed?

No. CORS support has been part of the framework since Laravel 9 via the built-in HandleCors middleware. The fruitcake/laravel-cors package is abandoned and should not be installed on modern releases.

Why did editing config/cors.php change nothing?

The configuration is cached. When config caching is active, changes are ignored until you run php artisan config:clear to drop the cache or php artisan config:cache to rebuild it.

Why are CORS headers missing on a route outside /api?

The default paths array only matches ['api/*', 'sanctum/csrf-cookie']. Routes outside those patterns receive no CORS headers from HandleCors unless you add matching patterns to the paths configuration.

When should the cors.dev proxy be used instead?

Use the cors.dev free tier for browser GET and HEAD calls to third-party public APIs on any public host; managed access adds write methods and custom API headers. When you control the Laravel backend, configure your own server headers.