vira/hpropagate
Vira HPropagate is a Laravel package for propagating selected HTTP headers to outgoing requests, helping keep correlation IDs and tracing context consistent across internal service calls. Simple configuration makes request context easier to forward.
vira/hpropagate is a Symfony bundle, not a Laravel package — there is no service provider, no config/hpropagate.php, and composer require vira/hpropagate will not auto-wire anything for a Laravel app. Treat it in one of two ways:
symfony/http-client rather than Guzzle, in which case you still need to register the bundle manually (Laravel doesn't run Kernel::registerBundles()), so you'd be constructing the listener and the client decorator by hand in a service provider.Where to look first, in this order:
DependencyInjection/Configuration.php — the exact config tree. This tells you the header allowlist key, whether there's a default set, and whether per-client scoping exists.kernel.request with priority and whether it filters to the master request.HttpClientInterface decorator — this is the piece actually doing the injection, and it's the piece you'd reimplement.First use case: correlation-ID propagation. Incoming X-Request-ID (or X-Correlation-ID) is read once per request and attached to every outbound call, so a single trace spans your gateway → your API → downstream services.
# config/packages/vira_h_propagate.yaml (Symfony side)
vira_h_propagate:
headers:
- X-Request-ID
- traceparent
The Laravel equivalent you'd actually ship:
// app/Providers/HttpServiceProvider.php
Http::globalRequestMiddleware(function ($request) {
$id = request()->header('X-Request-ID') ?? (string) Str::uuid();
return $request
->withHeader('X-Request-ID', $id)
->withHeader('traceparent', request()->header('traceparent', ''));
});
That single middleware reproduces ~90% of the bundle's value for a typical Laravel app.
Allowlist, never "propagate everything". The whole design of the bundle hinges on an explicit header list. Mirror that in Laravel — a hardcoded array, ideally in a config file, so you can review propagation changes in a diff rather than discovering them in production.
Capture once, at the edge. The bundle's listener grabs headers during kernel.request, before controllers run. In Laravel the analogue is a middleware that resolves the correlation ID, stores it (container binding, Context, or a request attribute), and also echoes it back on the response so clients can correlate.
final class CorrelationId
{
public function handle($request, Closure $next)
{
$id = $request->header('X-Request-ID') ?: (string) Str::uuid();
app()->instance('correlation.id', $id);
$response = $next($request);
return $response->header('X-Request-ID', $id);
}
}
Then read from the container in your HTTP middleware, not from request() — that keeps the value stable across sub-requests and works in queue workers.
Scope propagation per downstream. Don't blast Authorization or Cookie at third-party APIs. The bundle supports scoping the decorated client; in Laravel use named clients with their own middleware:
Http::macro('internal', fn () => Http::withMiddleware($propagateHeaders)->baseUrl(config('services.internal.url')));
Http::macro('stripe', fn () => Http::withToken(config('services.stripe.secret'))); // no propagation
Queue jobs need explicit handling. The bundle only covers in-request outbound calls. The moment a job dispatches, the incoming request is gone. Serialize the correlation ID into the job payload ($this->correlationId = app('correlation.id')) and rebind it in handle(). This is the single most common gap when teams "add correlation IDs".
Test the middleware, not the network. Assert on the request object, not on a real endpoint:
Http::fake();
$this->withHeader('X-Request-ID', 'abc-123')->get('/endpoint');
Http::assertSent(fn ($req) => $req->header('X-Request-ID')[0] === 'abc-123');
Migrating an existing Symfony service to Laravel: the bundle's config maps cleanly onto Http::globalRequestMiddleware. Keep the header names identical across both apps during the transition, otherwise traces break at the boundary — and that boundary is exactly where you need them.
X-Request-Id vs X-Request-ID). If a downstream does a strict string comparison, you'll get silent misses. Assert with $request->hasHeader('x-request-id'), never === on the raw name.kernel.request. If the listener doesn't check isMasterRequest()/isMainRequest(), you'll propagate headers from the wrong request. In Laravel, the equivalent is $request->is() on forwarded routes or Request::createFrom() in tests — verify which request object your middleware actually sees.app()->instance() binding set from a request survives into the next one unless reset. Use scoped() bindings (Laravel 10+) or resolve from request() at call time so a stale X-Request-ID doesn't leak between users.Authorization/trace headers unless the client strips them. Guzzle's allow_redirects config and Symfony's max_redirects both allow per-host header stripping — configure it, don't assume.traceparent to Sentry/Datadog ingestion endpoints is usually harmless but noisy, and sending X-Request-ID to a vendor API can confuse their rate-limit de-duplication. Keep the allowlist tight and the client list narrower.Log::debug('outbound headers', $request->getHeaders()) behind a config flag). Header propagation failures are almost always "the middleware wasn't attached to this client" — the log tells you instantly.composer update six months from now may well hit an abandoned package, and a Laravel app has no obligation to carry a Symfony bundle it barely uses.->withTraceHeaders() manually, because someone will forget and the trace will break silently.How can I help you explore Laravel packages today?