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

Stack Cors Laravel Package

asm89/stack-cors

Stack CORS is a middleware for StackPHP and Symfony HttpKernel apps that adds Cross-Origin Resource Sharing headers, handles preflight OPTIONS requests, and offers configurable origins, methods, headers, and credentials. Widely used and easy to integrate.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer:

composer require asm89/stack-cors

This is a framework-agnostic CORS implementation built on Symfony HttpFoundation Request/Response objects, originally designed for the stackphp middleware convention (__invoke($request, $next)-style wrapping). It ships two things:

  • Asm89\Stack\CorsService — the actual CORS decision logic (origin matching, preflight handling, header injection). This is what most integrators use, including Laravel's CORS middleware.
  • Asm89\Stack\CorsMiddleware — a thin stack middleware wrapper around the service.

Version/compatibility note: v2.4.0 only widened the dependency range to allow Symfony 8 — there are no code, config, or API changes. The package tracks symfony/http-foundation from 5.x through 8.x, so the PHP floor is whatever your Symfony version already resolves; nothing to do here unless you're mid-upgrade to Symfony 8, in which case the constraint is no longer the blocker in your composer why-not.

Look here first: open src/CorsService.php and read isPreflightRequest, handlePreflightRequest, and addActualRequestHeaders. The config array consumed by the constructor is the whole API surface, and it is identical to what barryvdh/laravel-cors / fruitcake/laravel-cors expose — so Laravel config knowledge transfers directly.

A minimal wiring, which is also the first use case (letting a browser SPA on another origin call your API with cookies):

use Asm89\Stack\CorsService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$cors = new CorsService([
    'allowedOrigins'     => ['https://app.example.com'],
    'allowedMethods'     => ['GET', 'POST', 'PUT', 'DELETE'],
    'allowedHeaders'     => ['Content-Type', 'Authorization', 'X-Requested-With'],
    'exposedHeaders'     => ['X-Total-Count'],
    'maxAge'             => 3600,
    'supportsCredentials' => true,
]);

$handler = function (Request $request) use ($cors, $app) {
    // 1. Short-circuit preflight — never hit the app/routing/auth for these.
    if ($cors->isPreflightRequest($request)) {
        return $cors->handlePreflightRequest($request);
    }

    // 2. Normal request: run the app, then decorate the response.
    $response = $app($request);

    return $cors->addActualRequestHeaders($request, $response);
};

That's the entire pattern: preflight short-circuit first, decorate real responses after.

Implementation Patterns

Keep CORS as the outermost middleware. Preflight OPTIONS requests carry no credentials and no session; if auth, CSRF, rate limiting, or routing run before the CORS layer, preflight fails with a 401/404 and the browser reports a generic CORS error. Register the middleware before the router, ideally before everything except request ID / logging.

Two-phase handling is the whole model. Do not try to attach CORS headers on a single pass. Preflight (OPTIONS + Access-Control-Request-Method) gets a synthetic response with no body; actual requests get Access-Control-Allow-Origin, Allow-Credentials, and Expose-Headers appended to the real response. Your integration is just those two branches.

Drive config from environment, per environment. Local/staging commonly use ['*'] for convenience; production should enumerate explicit origins. Keep supportsCredentials, allowedMethods, and allowedHeaders in one config file so security review has a single place to look. Cache the CorsService instance (build it once at boot), since origin matching is done per request.

Origin patterns. allowedOrigins entries containing * are matched as wildcards (e.g. https://*.example.com), which is how you handle multi-tenant subdomains without listing every tenant. Wildcard patterns are still an allowlist — but be aware that a wildcard like https://*.example.com can match unintended host suffixes if you're sloppy with the scheme/host separator.

Credentials and wildcards are mutually exclusive. If supportsCredentials is true, you cannot use '*' as an allowed origin — browsers reject Access-Control-Allow-Origin: * with credentials. The service will reflect the specific request origin instead, but you must list concrete origins (or patterns). Set supportsCredentials only when the client actually calls fetch(..., { credentials: 'include' }); leaving it true unnecessarily forces you off the wildcard.

Error responses still need CORS headers. Because addActualRequestHeaders runs after your app, an uncaught exception that escapes the middleware pipeline will produce a 500 with no CORS headers, and the browser will report a CORS failure rather than your error message. Convert exceptions to responses inside the pipeline (or handle them in the framework kernel) so they pass through the decorator. This is the single most common "why can't I see my API error in JS?" bug.

Reverse proxies and CDNs. If anything caches responses, verify Vary: Origin is present on decorated responses (and ideally Vary: Access-Control-Request-Method, Access-Control-Request-Headers on preflights). Without it, a cached response for origin A will be served to origin B with the wrong Access-Control-Allow-Origin.

Framework integration shortcuts.

  • Laravel: unless you are on a very old version, you are already getting this package transitively through fruitcake/laravel-cors (or the newer fruitcake/php-cors + framework HandleCors). Don't install it side-by-side unless you're replacing that layer.
  • Slim / Lumen / stackphp: wrap via Stack\Builder and push this middleware outermost.
  • Symfony HttpKernel: adapt CorsService into a kernel.request listener for preflight and a kernel.response listener for actual headers — same two branches as above. This wiring is unaffected by Symfony version: v2.4.0 added Symfony 8 to the allowed constraint without changing the HttpFoundation calls the listeners rely on, so the same listeners work on Symfony 6, 7, and 8.

Testing. Unit-test the service directly by constructing Request::create() with Origin and Access-Control-Request-Method headers and asserting the returned Response headers. For functional tests, always include a plain GET with Origin and an OPTIONS preflight — teams routinely test only one and ship a broken the other.

Gotchas and Tips

  • Duplicate headers from two layers. If nginx/Apache/CloudFront also injects Access-Control-Allow-Origin (or a framework bundle does), you get two values and browsers reject the response. Pick exactly one layer to own CORS.
  • OPTIONS without Access-Control-Request-Method is not a preflight. isPreflightRequest correctly requires that header; a bare OPTIONS should fall through to your normal routing (or return 405), not be silently answered.
  • allowedHeaders vs exposedHeaders are different things. allowedHeaders governs what the browser may send on the request; exposedHeaders governs what JS may read off the response. Custom response headers (pagination X-Total-Count, Location) are invisible to fetch until added to exposedHeaders.
  • Header name case. The spec is case-insensitive, but the library compares the config arrays you give it; normalize to lowercase and be consistent. If you don't list Content-Type / Authorization, they're simply not allowed.
  • maxAge is a hint, not a guarantee. It only affects preflight caching, in seconds, and browsers cap it (Chrome ~2 hours, Firefox ~24 hours). Setting it huge doesn't do what you think; 0 or negative disables caching entirely.
  • localhost ≠ 127.0.0.1, and ports count. http://localhost:3000 and http://localhost:5173 are different origins. This is the #1 "works for me" CORS cause in frontend teams.
  • Debug with curl, not just DevTools. The browser's CORS error hides the real HTTP status. Reproduce with:
    curl -i -X OPTIONS https://api.example.com/thing \
      -H "Origin: https://app.example.com" \
      -H "Access-Control-Request-Method: POST" \
      -H "Access-Control-Request-Headers: content-type,authorization"
    
    Then compare against a plain GET with just Origin.
  • Silent no-op configs. Empty allowedOrigins or a typo'd key in the config array produces no headers at all rather than an error. Validate/normalize the config at boot and fail loudly in non-production.
  • Maintenance mode, but constraints are kept current. The ecosystem has moved to fruitcake/php-cors, and the API here has been stable for years. Recent releases are dependency hygiene rather than new behaviour — v2.4.0 in particular only allows Symfony 8 (PR #109), with no code changes — so treat patch/minor bumps as drop-in and don't re-audit your CORS config on upgrade. What you shouldn't expect is new CORS-spec features. If you need dynamic per-tenant origin logic, wrapping or subclassing CorsService is the intended extension point — compute allowed origins before constructing the service rather than patching the header logic.
  • Upgrading to Symfony 8. If composer update was previously failing on asm89/stack-cors while you moved to Symfony 8, the fix is just bumping to ^2.4; no config or middleware changes are needed. Conversely, if you're pinned to Symfony 5/6, ^2.4 still resolves — the constraint was widened, not replaced.
  • Log rejected origins. When a preflight unexpectedly fails, the service just omits headers. Add a wrapper that logs Origin when isCorsRequest is true but the response lacks Access-Control-Allow-Origin — it turns a 30-minute browser debugging session into a grep.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core