m6web/request-headers-forwarder-guzzle-bundle
A Symfony bundle that adds a Guzzle plugin for forwarding incoming HTTP request headers onto outgoing Guzzle requests, helping propagate correlation IDs, authorization, and tracing headers across service calls with minimal configuration.
This bundle predates modern Symfony (it targets Symfony 2.x/3.x and Guzzle 5/6) and is archived, so treat the README and Resources/config/services.xml as the source of truth over any blog post.
Minimal setup:
Install — pin the last release instead of dev-master since the repo is frozen:
composer require m6web/request-headers-forwarder-guzzle-bundle:^1.0
If Composer refuses on a modern Symfony, you will need "minimum-stability": "dev" plus a replace/conflict tweak, or vendor the middleware class into your own code — there is no upstream fix coming.
Register the bundle — legacy AppKernel::registerBundles() or config/bundles.php if you have a compatibility layer.
Configure the header whitelist — the bundle's whole job is copying named headers off the current Request and onto outbound Guzzle calls:
m6_web_request_headers_forwarder_guzzle:
headers:
- Authorization
- X-Request-Id
- X-Correlation-Id
Wire the middleware onto a Guzzle client — the bundle exposes a class you push onto the client's HandlerStack before sending sub-requests:
$stack = HandlerStack::create();
$stack->push($container->get('m6_web_request_headers_forwarder_guzzle.middleware'), 'forward_headers');
$client = new Client(['handler' => $stack, 'base_uri' => 'https://internal-api/']);
First use case: a Symfony controller receives a request with Authorization and X-Request-Id, then calls an internal microservice via Guzzle. Without the bundle you re-implement header extraction per client; with it you configure once and every Guzzle client using that stack forwards the whitelisted headers automatically.
First place to look if something doesn't work: vendor/m6web/.../Resources/config/ for the real service ID, and the middleware class itself for how it handles multiple RequestStack entries.
Handler-stack middleware, not an event listener. Guzzle 5/6 middleware runs per-request, so the bundle is designed to be composed onto each outbound client. The idiomatic approach:
final class ApiClientFactory
{
public function __construct(
private RequestStack $requestStack,
private array $headerNames,
) {}
public function create(string $baseUri): Client
{
$stack = HandlerStack::create();
$stack->push($this->forwardHeadersMiddleware());
return new Client(['handler' => $stack, 'base_uri' => $baseUri]);
}
}
Centralize client creation. Because the middleware lives on a HandlerStack, every new Client needs it pushed. Wrap client construction in a factory (or a DI service definition per downstream service) so you can't forget.
Whitelist-only forwarding. Configure the bundle with the smallest set of headers you need. Typical production set: Authorization, X-Request-Id, X-Correlation-Id, Accept-Language. Do not forward Host, Content-Length, Cookie, or X-Forwarded-* — Guzzle and the upstream service will already manage those, and duplicating them causes signature/validation failures.
Correlation ID propagation. The strongest day-to-day use is distributed tracing: generate X-Request-Id in a kernel listener if absent, forward it to every downstream service, and log it at each hop. This bundle lets you do that without touching each call site.
Integrate with RequestStack, not the global $_SERVER. The bundle reads from the request stack, so it behaves in Symfony sub-requests (ESIs, HttpKernel::handle() calls) — but only if your whitelist matches the header names those sub-requests carry.
Compose with other middleware. Push this middleware before Guzzle's http_errors/retry middleware so retries re-read the same headers, and after authentication middleware if you build credentials via OAuth — otherwise you'll overwrite a freshly minted Authorization with the inbound one.
Testing pattern. In functional tests, set headers on the Symfony test client and assert the outgoing Guzzle request with a MockHandler pushed after this middleware. Verify the whitelist by asserting whitelisting works (i.e., a blacklisted header is not present on the outbound request).
Archived and unmaintained. No Symfony 5/6 support, no Guzzle 7 compatibility. Guzzle 7 changed the handler-stack middleware signature (callable receives RequestInterface, array $options, returns a PromiseInterface). The 2017-era bundle was written against Guzzle 5/6; check the exact version constraint in its composer.json before assuming it works. If you're on Guzzle 6, you're in the supported window; Guzzle 7 is a fork-it situation.
Header case sensitivity. HTTP header names are case-insensitive, but naive middleware implementations often do a strict case-sensitive match. If x-request-id is configured but the incoming request has X-Request-Id, it silently won't forward. Normalize to lowercase in both the config and the middleware if you fork it.
RequestStack may be empty. In CLI (console commands, queue workers), there is no current request. The middleware must no-op gracefully. If your worker processes jobs and makes Guzzle calls, don't assume headers exist — guard for a null getCurrentRequest().
withHeader vs. withAddedHeader. The correct behavior depends on context: use withHeader to replace, withAddedHeader to append multiple Accept-Language-style values. Read the middleware source to confirm which it does — getting this wrong can drop legitimate multi-value headers.
Security: forwarding Authorization is risky. If the outbound client targets a third-party API, you are sending your users' bearer tokens to someone else. Restrict this bundle to internal service clients, or build in a per-client header override so third-party clients skip the middleware entirely.
Debugging. Log the outbound headers at debug level in your client factory while onboarding. A quick sanity check: $stack ordering — if the middleware never runs, it was likely pushed so deeply it's overwritten, or the HandlerStack was replaced after construction.
Config cache. In older Symfony, config changes to the header list may require cache:clear. If a newly added header doesn't forward, that's the first thing to check.
Extension points when forking. The middleware is small; if you need conditional logic (e.g., only forward when the downstream host matches an allowlist), it's cheaper to copy the class into your app and drop the bundle than to fight its DI wiring. The concept — "read from RequestStack, apply to HandlerStack" — is ~30 lines and worth owning.
Modern alternative. For new code on Symfony 4+, a small custom Guzzle middleware reading RequestStack (or HttpClient's ScopingHttpClient/TraceableHttpClient) covers the same use case with active maintenance, so consider this bundle primarily as a reference implementation.
How can I help you explore Laravel packages today?