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

Inter App Request Identifier Bundle Laravel Package

juliendufresne/inter-app-request-identifier-bundle

A Symfony bundle that assigns and propagates unique identifiers for inter-application requests, making it easier to trace requests and correlate logs across services. Integrates with the Symfony request lifecycle and provides configuration for consistent request IDs.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. There is still no service provider, no config/*.php, and nothing in composer.json targeting illuminate/*. If you composer require it into a Laravel app you will get an autoloaded interface, a Guzzle middleware, a Monolog processor, and a DI extension class that never runs. Treat it as a reference implementation to read, not a dependency to install.

What it gives you is small but now slightly larger than before: an interface describing "this process has an identifier", plus a request listener that reads/writes that identifier on a header, and — as of v0.2.0 — the two integration pieces most teams hand-roll: a Guzzle middleware that propagates the header outbound, and a Monolog processor that stamps it onto every log record.

What changed in v0.2.0 (Activate Guzzle and Monolog by default if present): the bundle now auto-registers its Guzzle middleware and Monolog processor when those libraries are detected in the app, instead of requiring opt-in config. Two consequences worth noting before you read the source:

  1. There are now two new classes worth reading — the Guzzle middleware and the Monolog processor — because they encode the design decisions (which header, what fallback when no ID is set, whether the processor mutates extra or context) that you will otherwise guess wrong when you port them.
  2. "If present" means conditional service registration, which is a cache- and environment-sensitive behavior in Symfony and a complete no-op in Laravel.

Minimal steps to evaluate (15 minutes, up from 10):

  1. Open src/ on GitHub directly — still a handful of files. Read the interface first, then the listener, then the new Guzzle middleware and Monolog processor, then the DI extension last (it is the only file that explains the "if present" wiring).
  2. Note the header name it standardizes on and whether the identifier is generated inbound, honored from inbound, or both — and specifically what the Guzzle middleware does when no identifier is set (generate? omit the header? send an empty string?). That fallback is the part you will want to copy or deliberately diverge from.
  3. Note the Symfony/PHP version constraints in composer.json — the original targeted PHP 7.x and Symfony 3/4; check whether v0.2.0 moved them at all. A 0.2.0 bump with a one-line changelog is not a maintenance signal.
  4. Decide: adopt the concept in Laravel, or port the interface plus the two integrations into app/.

First use case: an API gateway calls a billing service, which calls a ledger service. A single customer-facing request touches three logs across three codebases. You want one string in every log line, every outbound header, and every queued job.

// app/Contracts/HasRequestIdentifier.php — your port of the bundle's interface
interface HasRequestIdentifier
{
    public function identifier(): string;
}
// app/Http/Middleware/AssignRequestIdentifier.php
final class AssignRequestIdentifier
{
    public const HEADER = 'X-Request-Id';

    public function handle(Request $request, Closure $next): Response
    {
        $inbound = $request->header(self::HEADER);
        $id = $this->isValid($inbound) ? $inbound : (string) Str::uuid();

        app()->instance('request.id', $id);
        Log::withContext(['request_id' => $id]);

        $response = $next($request);
        $response->headers->set(self::HEADER, $id);

        return $response;
    }

    private function isValid(?string $v): bool
    {
        return is_string($v) && preg_match('/^[A-Za-z0-9._-]{1,64}$/', $v) === 1;
    }
}

Register it in bootstrap/app.php ($middleware->prepend(...) on the web/api groups).

Implementation Patterns

Edge generation, interior trust. Generate the ID only at the system boundary (the public-facing app or gateway). Every internal service honors an inbound header if present and generates one only if absent. This keeps the ID stable across the whole call graph instead of forking a new one at each hop.

Outbound propagation — now a first-class concern. This is the pattern v0.2.0 promotes from "you should write this" to "the bundle ships this". In Laravel:

Http::globalRequestMiddleware(fn ($request) => $request->withHeader(
    AssignRequestIdentifier::HEADER,
    app()->bound('request.id') ? app('request.id') : (string) Str::uuid(),
));

For Guzzle, this is exactly what the bundle's middleware now does by default — HandlerStack::push(Middleware::mapRequest(...)) behind the scenes. Read their version before writing yours and decide deliberately: generate-on-missing (their likely default) or omit-the-header-when-unset. Generate-on-missing is friendlier to log correlation; omit-when-unset is friendlier when the same client talks to third parties (see the gotcha below).

Carry it through async work. Queue jobs, scheduled commands, and event listeners do not inherit the request context. Push the ID into the job payload and re-establish it in the worker:

final class SyncInvoice implements ShouldQueue
{
    public function __construct(
        public readonly int $invoiceId,
        public readonly string $requestId,
    ) {}

    public function handle(): void
    {
        Log::withContext(['request_id' => $this->requestId]);
        // ...
    }
}

Dispatch with SyncInvoice::dispatch($id, app('request.id')). Neither the Guzzle middleware nor the Monolog processor helps here — both are per-process/per-record and neither serializes context into a payload. This remains the highest-value thing you get out of the pattern and still the most commonly skipped.

Log processor over per-call context. Also newly first-class: the bundle's Monolog processor is the reference for this. A Monolog processor that pulls from the container applies everywhere, including framework-generated log lines, which Log::withContext() in controllers never covers. Port theirs (read how it handles the missing-context case — that is the part that bites in console commands) or use Laravel's equivalent:

// app/Logging/RequestIdProcessor.php
public function __invoke(LogRecord $record): LogRecord
{
    $record->context['request_id'] ??= app()->bound('request.id')
        ? app('request.id')
        : null;

    return $record;
}

Interface over global. The bundle's real contribution is the HasRequestIdentifier contract rather than a magic global. Binding an implementation in the container ($this->app->bind(HasRequestIdentifier::class, ...)) lets you swap a UUID generator for a deterministic one in tests, and lets domain services depend on the contract instead of reading request()->header() directly. Do this if you have more than two or three call sites; skip it for a single middleware.

Gotchas and Tips

The release is a convenience, not a revival. v0.2.0 wires up Guzzle and Monolog, which is genuinely the right next feature for this package — but it is still a 0.x release of an unmaintained bundle. There is no Laravel support, no PHP 8 guarantee, no Symfony 5/6/7 work, and no security response. Do not put it in composer.json; copy the interface and the two new integration classes and move on. Nothing here exceeds a hundred lines.

"By default if present" is a cache-sensitive conditional. Symfony's DI extension almost certainly guards registration with class_exists() / ContainerBuilder::willBeAvailable(). That decision is baked into the compiled container, so: installing or removing Guzzle or Monolog requires a cache:clear, and behavior differs between dev and prod, and between machines with different composer install --no-dev states. If you are debugging "why is my header missing in CI but not locally", start here.

New default = silent behavior change on upgrade. The whole point of "activate by default" is that upgrading changes what your app sends, without you editing config. If a Guzzle middleware starts attaching X-Request-Id to every outbound request, it now also attaches it to Stripe, Mailgun, and whatever else you call. Review your outbound clients before bumping; consider scoping the middleware to internal hosts only.

DI-configured clients only, for the Guzzle piece. A middleware registered through the bundle is attached to the clients the container builds. new Client() anywhere in your code, or a Laravel HTTP client with its own handler stack, bypasses it entirely. Half your outbound calls carrying the header is worse than none — it produces traces that look complete until they hit the gap. Centralize via Http::globalRequestMiddleware() and one Guzzle factory.

The Monolog processor changes your log schema. Every record now gains a field. Log parsers, dashboards, alerting rules, and any json log drain with a strict schema will see it. And in console/queue contexts with no request, check what the processor emits — null, an empty string, or an exception on a missing service. Decide and document; do not inherit it by accident.

Never trust the inbound header blindly. An attacker who controls the value can forge correlation across tenants, inject newlines into log files (log forging), or blow up your log index with 10 KB IDs. Validate with a strict regex and a length cap, and reject/regenerate on failure — as in the middleware above. Some teams also strip the header at the public edge entirely and only honor it on internal service-to-service routes. A shipped Guzzle middleware does not do this validation for you.

Header name collisions. The ecosystem is converging on W3C traceparent and X-Correlation-ID. If you already run OpenTelemetry or a service mesh, adding a competing X-Request-Id gives you two identifiers that disagree and no way to join them. Either reuse the trace ID or emit both with a documented relationship.

The ID is not an identity. It is a correlation token. Do not use it for idempotency keys, tenant scoping, authorization, rate limiting, or anything security-relevant — it is client-controllable by design.

Context leaks in long-running workers. Octane and queue workers reuse the process across requests and jobs. If you set the ID in the container and never reset it, job N+1 logs under job N's ID — and with a Monolog processor registered globally, that stale ID now lands on every line instead of just the ones you remembered to annotate. Reset in a finally block or use Context::forget() in a terminating hook.

Freeze it in tests. Bind a fixed ID in setUp() so assertions on log output or outbound headers are deterministic. Random UUIDs in tests are a flakiness source — and with Guzzle propagation on by default, Http::fake() assertions on headers will now fail nondeterministically unless you do.

Extension points worth keeping. The interface (HasRequestIdentifier with identifier(): string) and the two integration classes are the reusable artifacts. Vendor-copy them into app/Contracts/ and app/Logging/, bind one implementation, and you have the bundle's value with none of its maintenance risk. Note the source and the version you copied from so future readers — and future you — can diff against upstream.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource