Files
httpclient/docs/adr/ADR-002-request-id-propagation.md
Rene Nochebuena 6026ab8a5e feat(httpclient): initial stable release v0.9.0
Resilient HTTP client with circuit breaking, exponential-backoff retry, X-Request-ID propagation, and a generic typed JSON helper.

What's included:
- Client interface with Do(req) method; New(logger, cfg) and NewWithDefaults(logger) constructors
- Config struct with env-tag support for timeout, dial timeout, retry, and circuit breaker parameters
- Retry via avast/retry-go/v4 with BackOffDelay; triggers only on network errors and HTTP 5xx
- Circuit breaker via sony/gobreaker wrapping the full retry loop; open circuit → xerrors.ErrUnavailable
- X-Request-ID header propagated automatically from context via logz.GetRequestID on every attempt
- DoJSON[T](ctx, client, req) generic helper for typed JSON request/response with xerrors error mapping
- MapStatusToError(code, msg) exported function mapping HTTP status codes to xerrors types

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-19 13:04:37 +00:00

2.2 KiB

ADR-002: Request ID Propagation via X-Request-ID Header

Status: Accepted Date: 2026-03-18

Context

In a distributed system, a single inbound request may fan out to multiple downstream service calls. Without a shared correlation identifier, tracing a request across service logs requires matching timestamps or other heuristics. A request ID, propagated as an HTTP header (X-Request-ID), lets logs across services be correlated by a single value.

The logz module owns the request ID context key (ADR-003 global: context helpers live with data owners). httpclient depends on logz and should use its helpers rather than define its own context key.

Decision

Inside the retry function in Do, before executing each request attempt, the client reads the request ID from the context using logz.GetRequestID(req.Context()). If a non-empty value is present, it is set as the X-Request-ID header on the outgoing request:

if id := logz.GetRequestID(req.Context()); id != "" {
    req.Header.Set("X-Request-ID", id)
}

The header is set on every retry attempt, not just the first, because the same *http.Request object is reused across retries.

If no request ID is present in the context (the ID is the zero string), the header is not set. This is verified by TestClient_Do_NoRequestID.

Consequences

Positive:

  • Request IDs flow automatically to downstream services without any caller boilerplate.
  • Correlation across service boundaries works with no additional middleware.
  • The integration is testable: TestClient_Do_InjectsRequestID verifies end-to-end propagation using logz.WithRequestID and an httptest.Server.

Negative:

  • httpclient takes a direct import dependency on logz. This is accepted per ADR-001 (global) which permits direct imports between framework modules.
  • The header name X-Request-ID is hardcoded. Projects that use a different header name (e.g. X-Correlation-ID) cannot configure this without forking the client.
  • Header propagation only works when the caller places the request ID in the context via logz.WithRequestID. Requests built without a context carrying a request ID will not have the header set.