In a fresh Laravel 13 app with no config/cors.php, what answers CORS requests, and how do you change its settings?
answer
- global middleware, not a route option
- Illuminate\Http\Middleware\HandleCors
- defaults live in the framework's cors.php
- php artisan config:publish cors
- kept keys replace defaults wholesale
basics
~20 sLaravel's HandleCors middleware answers CORS requests: it sits in the default global stack and reads the cors config. A new app runs on the framework's built-in defaults until you run php artisan config:publish cors and edit config/cors.php.
solid answer
~40 s`Illuminate\Http\Middleware\HandleCors` is part of the global middleware stack Laravel 13 builds by default, so every request passes through it without any registration. On each request it reads `config('cors')`: if the path matches `paths`, it answers preflight `OPTIONS` requests itself and adds `Access-Control-*` headers to actual responses. A fresh app has no `config/cors.php`, so the values come from the framework's own file: `paths` `['api/*', 'sanctum/csrf-cookie']`, every origin, method and header allowed, `exposed_headers` empty, `max_age` `0`, `supports_credentials` `false`. To change them, run `php artisan config:publish cors`, which copies that file into `config/`, and edit it. Any top-level key you keep replaces the framework's value outright, and any key you delete falls back to the default.
code
bash · 1 linephp artisan config:publish corsgo deeper
Name HandleCors, say it is global, and know that php artisan config:publish cors creates config/cors.php. Recall the default paths value.
Explain that the framework's cors.php supplies the defaults, that published keys override them at the top level, and that HandleCors answers preflights before routing runs.
Treat config/cors.php as security configuration: narrow allowed_origins per environment through env(), re-check paths after trimming it, and know the skipWhen hook exists.
Weigh leaving CORS to the framework's defaults against a published, reviewed file. Decide who owns that file when several front ends call the same API.
## Where CORS lives in a Laravel 13 app **Cross-Origin Resource Sharing (CORS)** is the browser protocol that decides whether a page served from one origin (scheme, host and port) may read a response from another. On the server side, Laravel handles it with one class: `Illuminate\Http\Middleware\HandleCors`. `HandleCors` is part of the **global middleware stack** that `Illuminate\Foundation\Configuration\Middleware` builds by default, so it wraps every HTTP request the application handles. You do not register it in `bootstrap/app.php`, attach it to a route group or give it an alias. In the default global list it comes after `TrustProxies` and before `PreventRequestsDuringMaintenance`, `ValidatePostSize`, `TrimStrings` and `ConvertEmptyStringsToNull`. The CORS decisions themselves are delegated to a `CorsService` from the `fruitcake/php-cors` library, which `laravel/framework` requires. On every request, `HandleCors` hands that service the `cors` configuration array. ## The default configuration A new Laravel 13 app has **no `config/cors.php`**. Since Laravel 11's slim skeleton, the app ships only the config files most projects edit. `cors.php` is one of the opt-in files, alongside `broadcasting`, `hashing` and `view`. Until you publish it, the values come from the framework's own `config/cors.php`: | Key | Framework default | What it controls | |---|---|---| | `paths` | `['api/*', 'sanctum/csrf-cookie']` | Which requests `HandleCors` handles at all | | `allowed_methods` | `['*']` | Methods a preflight may approve | | `allowed_origins` | `['*']` | Origins allowed to read responses | | `allowed_origins_patterns` | `[]` | Extra origins matched by pattern | | `allowed_headers` | `['*']` | Request headers a preflight may approve | | `exposed_headers` | `[]` | Response headers the calling script may read | | `max_age` | `0` | Seconds a browser may cache a preflight answer | | `supports_credentials` | `false` | Whether `Access-Control-Allow-Credentials: true` is sent | Out of the box, then, any origin may call anything under `api/*` without credentials. That suits a public, token-authenticated API. It is too loose for most apps whose browser front end sends cookies, and too narrow for endpoints outside `api/*`. ## Publishing and overriding 1. Run `php artisan config:publish cors`. It copies the framework's file to `config/cors.php`. If the file already exists it refuses to overwrite it, unless you pass `--force`. 2. Edit the keys you need. Most often that is `allowed_origins`, `paths` and `supports_credentials`. 3. Commit the file. It is now ordinary application configuration. How your file combines with the framework's matters. At boot, Laravel's `LoadConfiguration` bootstrapper merges each app config file over the framework file of the same name, using a **top-level** merge: - a key you **leave out** of `config/cors.php` falls back to the framework default; - a key you **keep** replaces the default **wholesale**. Laravel does not merge the inside of any `cors` key a second time, so a `paths` array of `['api/*']` drops `sanctum/csrf-cookie`; - values that differ per environment, such as the front end's origin, are read with `env()` inside this file and nowhere else. To see the values Laravel will actually use after that merge, run `php artisan config:show cors`. It prints the effective `cors` array, which is quicker than reasoning about which keys your file kept. A published file that repeats every framework key (as `config:publish` produces it) is the easiest to review, because the reader never has to remember the defaults. ## What HandleCors does on each request 1. It runs any callbacks registered with `HandleCors::skipWhen()`. If one returns true, the request passes straight on. 2. It checks the request path against `paths`. With no match, the response gets no CORS headers of any kind. 3. It loads `config('cors')` into the CORS service. 4. If the request is a **preflight** (an `OPTIONS` request carrying `Access-Control-Request-Method`), `HandleCors` answers it directly. The framework's tests expect a `204` with an empty body. The preflight never reaches routing, route middleware or a controller. 5. Otherwise it calls the rest of the pipeline, then adds the `Access-Control-*` headers to whatever response comes back. That includes error responses rendered by the exception handler. ## Mistakes to avoid - **Looking for CORS in `app/Http/Kernel.php`.** A Laravel 11+ app has no such class; the global stack is configured in `bootstrap/app.php`. - **Installing a CORS package or writing a `Cors` middleware** for a job the framework already does. - **Declaring `OPTIONS` routes for preflights.** `HandleCors` answers preflights for every path it covers. - **Editing the file under `vendor/`.** Composer overwrites it on the next update; publish the file instead. - **Forgetting that a published key replaces its default outright.** Re-check `paths` after trimming it. - **Treating the defaults as production-ready for a cookie-based front end.** `allowed_origins` of `['*']` with `supports_credentials` `false` suits public token calls, not session cookies.
- Do you need to define OPTIONS routes so preflights succeed in Laravel?No. For any path listed in `paths`, `HandleCors` recognises the preflight and returns the answer itself, before routing runs. The framework's own tests get a `204` for an `api/` URI that has no route at all. Route middleware such as `auth:sanctum` never sees the preflight.
- What happens when your published config/cors.php leaves out a key such as max_age?Laravel merges each app config file over the framework's file of the same name at the top level. A missing key therefore falls back to the framework default, which is `0` for `max_age`. The reverse holds too: a key you keep replaces the default entirely, so a trimmed `paths` array loses `sanctum/csrf-cookie`.
- How can you make HandleCors ignore certain requests without changing config/cors.php?Register a callback with `HandleCors::skipWhen(fn ($request) => ...)`, typically in a service provider's `boot()`. `HandleCors` runs these callbacks before anything else, and when one returns `true` it hands the request on without adding any CORS headers. `HandleCors::flushState()` clears the registered callbacks.
saying these in an interview costs you the question
- HandleCors must be added to a route group or alias before it does anything.
- CORS is configured in app/Http/Kernel.php in a current Laravel app.
- A new Laravel app has no CORS support until a package is installed.
- The way to change CORS settings is to edit vendor/laravel/framework/config/cors.php.
- Preflights fail unless OPTIONS routes are declared in routes/api.php.