skip to content

CORS Paths & Origins

HandleCors in the global stack answers preflights from config/cors.php, published with config:publish cors. Interviewers probe why a browser still blocks a cross-origin call.

on this pageshow

explore

questions

5

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.
open as a page

In Laravel's config/cors.php, what does the paths key decide, and why can a correctly listed origin still get no CORS headers?

level: middleimportance: must knowfreq 48%

basics

~20 s

The paths key decides which requests HandleCors handles at all, and it is checked first. A request whose path matches no entry gets no Access-Control headers whatever allowed_origins says, so the browser blocks a cross-origin call to it.

open as a page

When a request comes from an origin missing from Laravel's allowed_origins, does HandleCors stop it before the controller runs?

level: middleimportance: should knowfreq 38%

basics

~20 s

No. For an actual request HandleCors always passes it on, so routing, middleware and the controller run. HandleCors only decides which Access-Control headers go on the response, and the browser then withholds that response from the calling page.

open as a page

Your Laravel API must accept cookie-authenticated calls from a marketing site on another origin — what must change in config/cors.php, and why?

level: seniorimportance: should knowfreq 42%

basics

~10 s

Set supports_credentials to true so HandleCors sends Access-Control-Allow-Credentials: true. Replace '*' in allowed_origins with the marketing site's exact origin, and list every credentialed path in paths, including sanctum/csrf-cookie and login.

open as a page

Your Laravel API sends CORS headers on normal responses, yet the browser reports CORS errors only on some failed requests — what causes that?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Those failing responses never pass through HandleCors: PHP fatal errors rendered at shutdown, rejections by the web server or proxy before PHP runs, and failures in global middleware ahead of HandleCors. Exceptions the pipeline handles still get headers.

open as a page