skip to content

In a fresh Laravel 13 app with no config/cors.php, what answers CORS requests, and how do you change its settings?

level: juniorimportance: must knowfreq 55%

answer

  1. global middleware, not a route option
  2. Illuminate\Http\Middleware\HandleCors
  3. defaults live in the framework's cors.php
  4. php artisan config:publish cors
  5. kept keys replace defaults wholesale

basics

~20 s

Laravel'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 line
bash
php artisan config:publish cors

go deeper

for a junior

Name HandleCors, say it is global, and know that php artisan config:publish cors creates config/cors.php. Recall the default paths value.

for a middle

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.

for a senior

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.

for a principal

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.