Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Cors Laravel Package

tenantcloud/laravel-cors

A Laravel package for handling Cross-Origin Resource Sharing (CORS). It provides middleware and configuration to control allowed origins, methods, headers, and credentials, helping APIs respond safely to browser preflight and cross-origin requests.

View on GitHub
Deep Wiki
Context7

Getting Started

Version note: This assessment reflects v3.0.0, which is a breaking release — it requires Laravel 12 and PHP 8.4+. If you're on Laravel 10/11 or PHP 8.2/8.3, stay on the 2.4.x line (composer require "tenantcloud/laravel-cors:^2.4"); there is no compatibility shim or polyfill in v3.

  1. Confirm your runtime before anything else:

    php -v          # must be >= 8.4
    php artisan --version   # must be 12.x
    
  2. Install:

    composer require tenantcloud/laravel-cors
    
  3. Publish the config and read it — config/cors.php is the first file to look at, not the README:

    php artisan vendor:publish --provider="TenantCloud\Cors\CorsServiceProvider" --tag="cors"
    

    The keys that matter day-to-day: paths, allowed_origins, allowed_origins_patterns, allowed_methods, allowed_headers, exposed_headers, max_age, supports_credentials.

  4. Register the middleware. On Laravel 12 the skeleton uses bootstrap/app.php:

    ->withMiddleware(function (Middleware $middleware) {
        $middleware->prepend(\TenantCloud\Cors\HandleCors::class);
    })
    

    prepend matters — CORS headers must be added even when the request is later rejected by auth, throttle, or a 404.

  5. First use case: a separately-hosted SPA (or partner integration) calling your API. Set paths => ['api/*', 'sanctum/csrf-cookie'], put your front-end origin in allowed_origins, then verify with:

    curl -i -X OPTIONS https://api.test/api/orders \
      -H "Origin: https://app.test" \
      -H "Access-Control-Request-Method: POST" \
      -H "Access-Control-Request-Headers: content-type,authorization"
    

    You should see Access-Control-Allow-Origin and Access-Control-Allow-Methods on a 204. If you don't, the middleware isn't running on that path — check paths first.

Implementation Patterns

Global, path-scoped config. Keep one global middleware registration and scope by URL rather than per-route:

'paths' => ['api/*', 'oauth/token', 'storage/*'],

This is the lowest-friction pattern and survives route refactors. Reach for per-route middleware only when a few endpoints need genuinely different origins.

Per-route middleware for public vs. internal APIs. Split config into two files and swap at the route level:

Route::middleware('cors:public')->prefix('v1/public')->group(...);
Route::middleware('cors:internal')->prefix('v1/internal')->group(...);

This keeps a partner-facing allowlist small — never let a broad api/* origin list cover endpoints that accept session cookies.

Dynamic / multi-tenant origins. Tenant-scoped SaaS apps can't enumerate customer domains in a config file. Resolve the origin from the request instead of listing it:

'allowed_origins_patterns' => [
    '#^https://([a-z0-9-]+)\.app\.test$#',
],

…or use a closure in allowed_origins that looks up the tenant's verified domains. Keep the closure cheap (cached lookup, no N+1) — it runs on every matched request, including preflights.

Credentials + SPA auth. Cookie/session-based SPAs need both sides set:

'supports_credentials' => true,
'allowed_origins' => ['https://app.test'],
'allowed_headers' => ['*'],
'exposed_headers' => ['X-Request-Id', 'RateLimit-Remaining'],

Anything the browser JS needs to read (pagination headers, request IDs, rate limits) must be in exposed_headers — otherwise it's invisible to your front-end even though it's on the wire.

Preflight bypass and route hygiene. Preflights carry no credentials, so any auth/can middleware on the route will 401 the OPTIONS request. Either let the CORS middleware short-circuit preflights (prepend it, as above) or register explicit Route::options(...) handlers. In practice: prepend and don't add auth to preflights.

Testing pattern. Assert headers explicitly, in both directions:

$this->withHeaders(['Origin' => 'https://app.test'])
    ->options('/api/orders')
    ->assertNoContent()
    ->assertHeader('Access-Control-Allow-Origin', 'https://app.test');

$this->withHeaders(['Origin' => 'https://evil.test'])
    ->getJson('/api/orders')
    ->assertHeaderMissing('Access-Control-Allow-Origin');

The negative test is the one that catches real misconfiguration.

Local dev with vite/localhost on varying ports. Add the port-bearing origins explicitly, or use a pattern for http://localhost:5\d{3}. Don't loosen production config to make local dev work — gate on app()->environment('local').

Gotchas and Tips

  • * and supports_credentials are mutually exclusive. The browser silently rejects Access-Control-Allow-Origin: * when credentials are included. With supports_credentials => true you must echo a concrete origin.

  • Trailing slashes break exact matches. https://app.test/ ≠ https://app.test. Same for header casing in patterns.

  • Only one origin is echoed. The package reflects the matched request origin — do not expect a comma-separated allowlist in the response header; browsers reject that.

  • Vary: Origin is essential behind a CDN. If your origin allowlist is per-request (patterns, closures, multi-tenant), confirm the response carries Vary: Origin, or a cache will serve one tenant's CORS headers to another. This is the classic multi-tenant CORS leak.

  • Config caching in production. php artisan config:cache is required after edits; a stale cached cors.php is the #1 cause of "works locally, fails in prod". Run php artisan config:clear when debugging.

  • Exception responses can bypass CORS. A fatal thrown before the middleware stack produces a 500 with no CORS headers, which the browser reports as a generic CORS error and hides the real cause. Check the server log, not the console.

  • Octane workers pin config per worker. After deploying new CORS config under Octane, php artisan octane:reload; otherwise some workers serve the old allowlist.

  • Debugging checklist, in order:

    1. Is the middleware actually in the stack? (php artisan route:list -v for the route's middleware)
    2. Does the path match paths? (api/* doesn't match /api itself)
    3. Is the origin in allowed_origins/patterns — exactly, including scheme and port?
    4. Is it a preflight (no Access-Control-Allow-Origin but a 4xx)? Then auth is intercepting.
    5. Cached config?
  • Upgrading 2.4.x → 3.0.0: the diff is a platform-support bump, not an API rewrite, so the migration is mostly environmental — bump PHP to 8.4 and Laravel to 12 first, in a branch, then bump the constraint and re-run your CORS header tests. If you carry a forked config, diff it against the freshly published v3 config/cors.php; keys you rely on may have moved.

  • Extension points: the config file is the supported surface. If you need per-request decisions the patterns can't express (e.g. origins from a database with wildcard subdomains), resolve them in the allowed_origins closure rather than subclassing the middleware — subclassing couples you to internals that this major-version bump just proved can move.

  • Pin the major version (^3.0) and add a CI matrix job for PHP/Laravel so the next platform bump fails loudly at install time instead of at runtime.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings