mmoreram/http-headers-bundle
A Symfony bundle by Marc Morera for managing HTTP response headers through configuration and annotations, letting you define cache, security, and custom headers declaratively instead of setting them manually in every controller.
Install the bundle with Composer, pinning a version compatible with your Symfony major (the last tag, v1.0.x, was cut against Symfony 3.x, so on Symfony 4/5 expect to vendor-patch the bundle class):
composer require mmoreram/http-headers-bundle
Register it. On Symfony ≤3 in app/AppKernel.php:
$bundles = [
// ...
new Mmoreram\HTTPHeadersBundle\HTTPHeadersBundle(),
];
On Symfony 4+ add it to config/bundles.php (the bundle class is a plain Bundle subclass, so autoload works as long as the event-dispatcher API in its listener still matches your Symfony version — check Resources/config/services.yml and the listener class before relying on it).
Then drop a config file (Symfony ≤3: app/config/config.yml; Symfony 4+: config/packages/http_headers.yaml):
mmoreram_http_headers:
headers:
X-Frame-Options: "SAMEORIGIN"
X-Content-Type-Options: "nosniff"
Referrer-Policy: "strict-origin-when-cross-origin"
Look here first when something is off:
DependencyInjection/Configuration.php — the authoritative config tree; the exact root alias and the headers key shape live here, so verify against your installed version rather than trusting a blog post.EventListener/ (or Subscriber/) — the kernel.response listener that actually mutates the Response.Resources/config/services.yml — the listener's priority and whether it's wired as a tagged subscriber.First day-to-day use case: a global security-header floor. Put the four headers every browser-facing app should have (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and HSTS on TLS-only prod) in the prod config, one curl -I https://your-app/ to confirm, done. That is the entire value proposition — it keeps header policy out of PHP.
Environment-scoped policy. Keep dev permissive and prod strict by defining the bundle config only in the prod config and letting dev inherit nothing:
# config/packages/prod/http_headers.yaml
mmoreram_http_headers:
headers:
Strict-Transport-Security: "max-age=31536000; includeSubDomains; preload"
Content-Security-Policy: "default-src 'self'"
HSTS and CSP in dev will silently break your own debugging (cached HSTS on localhost:8000 bites you for weeks), so never let them leak below prod.
Layer it under controller-level overrides. Treat the bundle as a default and let controllers own exceptions — the pattern that works is: bundle sets the baseline, controller calls $response->headers->set('Content-Security-Policy', $perRouteCsp) for anything that genuinely differs. Understand which one wins: if the listener uses set(), the bundle runs after the controller and clobbers it; if it uses a "only if absent" guard, the controller wins. Check the listener before designing around overrides.
API vs HTML split. The bundle applies headers unconditionally, which is fine for X-Content-Type-Options but wrong for Content-Security-Policy on a JSON API. Two clean approaches:
Content-Type after the bundle's listener runs (needs a lower priority).Sane listener ordering. The bundle registers on kernel.response. Anything that reads or rewrites response headers (your own security listener, a cache-control normalizer, a CORS listener) needs to be ordered relative to it. Give the bundle an explicit priority in its service definition if you must, or your listener a priority of -1 to run after.
Compose with framework / reverse proxies. If a CDN or Varnish sits in front, the headers you set here are only half the story — the edge must be configured to forward them (Vary, and don't let the edge strip HSTS). The bundle is a convenience, not a substitute for edge config.
Test it like a contract. A single functional test per environment asserts the headers, which catches config drift during upgrades:
$client = static::createClient();
$client->request('GET', '/');
$response = $client->getResponse();
$this->assertSame('nosniff', $response->headers->get('X-Content-Type-Options'));
Symfony\Component\HttpKernel\Event\FilterResponseEvent → ResponseEvent rename in Symfony 4.3 will break the listener. Budget for a small vendor patch or, honestly, a 20-line kernel.response subscriber that does the same thing with no dependency.max-age: 0 to an integer and you'll get a confusing set() mismatch; quote everything.Set-Cookie and other multi-value headers need to be passed as arrays — a scalar value will replace, not append, and the bundle may not support arrays at all depending on version. Verify in the listener.curl -sI -o- -D- http://localhost:8000/ and $response->headers->all() dumped from a throwaway controller; don't guess.dump() in the listener or use Xdebug — there's no built-in trace.How can I help you explore Laravel packages today?