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

Cors Bundle Laravel Package

wernerdweight/cors-bundle

A Symfony bundle for adding Cross-Origin Resource Sharing (CORS) support to PHP applications. It helps configure allowed origins, headers, and methods so browsers can securely call your APIs and web endpoints, with setup handled through standard Symfony bundle configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

First, a reality check: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/cors.php, and no HandleCors middleware. If you are working in a Laravel codebase, close this tab and use the framework's built-in HandleCors middleware (Laravel 9+) or fruitcake/laravel-cors on older versions — do not try to bolt this on. The notes below assume a Symfony 5.4/6.x app.

Minimal steps:

  1. composer require wernerdweight/cors-bundle
  2. Register it manually in config/bundles.php — small bundles like this (2 stars, no Flex recipe) are usually not auto-registered:
    Wernerdweight\CORSBundle\WernerdweightCORSBundle::class => ['all' => true],
    
  3. Look here first: vendor/wernerdweight/cors-bundle/DependencyInjection/Configuration.php. With a package this small, the config tree is the documentation. Then check bin/console config:dump-reference <alias> (try cors, wernerdweight_cors, or whatever getAlias() returns) to see the real, validated option names rather than guessing from the README.
  4. Create config/packages/cors.yaml with a dev-friendly starting point — allowed origin http://localhost:3000, methods GET, POST, PUT, DELETE, OPTIONS, and the headers your SPA actually sends (Content-Type, Authorization).
  5. First use case to validate: a local SPA calling your API. Confirm with two curls, because a passing GET does not prove preflight works:
    curl -i -X OPTIONS http://localhost:8000/api/things \
      -H "Origin: http://localhost:3000" \
      -H "Access-Control-Request-Method: POST" \
      -H "Access-Control-Request-Headers: content-type,authorization"
    
    curl -i http://localhost:8000/api/things -H "Origin: http://localhost:3000"
    
    You want a 2xx/204 on the first with Access-Control-Allow-Origin, -Methods, -Headers, and the second to echo the origin plus Vary: Origin.

Implementation Patterns

Config is the API. A CORS bundle this thin is essentially a kernel event listener reading a config array. Structure your work around environment-specific YAML rather than code:

# config/packages/cors.yaml — the allowlist, safe everywhere
cors:
  allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
  allow_methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
  allow_headers: ['Content-Type', 'Authorization', 'X-Requested-With']
  expose_headers: ['X-Total-Count', 'Link']
  allow_credentials: true
  max_age: 3600
# config/packages/dev/cors.yaml — permissive locally, never in prod
cors:
  allow_origin: ['*']

Keeping allow_credentials: true and a wildcard origin in separate files prevents the classic "it worked on my machine" production break.

Preflight is the pattern that matters. Browsers send OPTIONS with no body and no auth. Many small CORS bundles respond on kernel.response but never short-circuit kernel.request, which means the request still travels through routing (404 for an undefined OPTIONS route) and through the security firewall (401 for an unauthenticated preflight). If your preflight returns 404 or 401, add a small listener at high priority:

class CorsPreflightListener
{
    public function onKernelRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();
        if (!$request->isMethod('OPTIONS')) {
            return;
        }
        $response = new Response(null, Response::HTTP_NO_CONTENT);
        // The bundle's response listener will decorate this with CORS headers.
        $event->setResponse($response);
    }
}

Register it with priority: 1024 so it runs before FirewallListener and the router.

Integrate at the edge, not per-controller. Let the bundle handle global headers and reserve per-route overrides for genuinely different surfaces (a public read-only API vs. an authenticated admin API behind a different origin). If the bundle has no per-route support, express the distinction by firewall/path config rather than duplicating logic in controllers.

Testing. Add one functional test asserting the header contract, so a future Symfony upgrade that changes event ordering fails loudly:

$this->client->request('OPTIONS', '/api/things', server: [
    'HTTP_ORIGIN' => 'http://localhost:3000',
    'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'POST',
]);
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('Access-Control-Allow-Origin', 'http://localhost:3000');

Extension point of last resort. If config doesn't cover a case (dynamic origins from a tenant table, say), decorate or replace the bundle's listener in services.yaml rather than editing vendor code — with 0 dependents and a 2023 release, you will be the only person maintaining your fork.

Gotchas and Tips

  • Duplicate headers. If nginx/Apache/Cloudflare also emits Access-Control-Allow-Origin (a common default in shared hosting panels), the browser rejects the response for having two values. Pick one layer. Check with curl -i | grep -i access-control.
  • * + credentials is illegal. allow_credentials: true with allow_origin: ['*'] is silently ignored by browsers. You must reflect a specific origin from an allowlist — and never reflect arbitrary Origin input unvalidated.
  • Always set Vary: Origin when you reflect an origin. Without it, a CDN or Symfony HttpCache will serve a response cached from origin A to origin B.
  • Preflight ordering vs. the security firewall is the single most common failure. A 401 on OPTIONS means your listener priority is too low or the firewall pattern matches /api for all methods.
  • Routing 404s on OPTIONS. Some router configs won't match OPTIONS on a GET-only route. The preflight short-circuit above sidesteps this entirely.
  • Don't guess config keys. The - vs _ naming in these micro-bundles is inconsistent. Run bin/console debug:config <alias> after editing — it fails fast on typos instead of silently falling back to defaults.
  • Caching of max_age. max_age: 3600 means origin/method changes take an hour to reach users. Use a low value (600) during rollout.
  • Maintenance risk is the headline gotcha. 2 stars, 0 dependents, last release Feb 2023. Nothing here is hard to replace: if you hit an edge case, the standard answer in the Symfony ecosystem is nelmio/cors-bundle, which is actively maintained and handles preflight, Vary, and origin regexes correctly. Budget an hour to swap rather than a day debugging.
  • Laravel developers, stop at the top. If you're in a mixed shop and someone added this to a Laravel composer.json, verify nothing bootstraps it (Symfony bundles are inert in Laravel, which is a good sign — but you still want the HandleCors middleware active, since an inert package provides zero CORS behavior while creating a false sense that it's configured).
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib