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/
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_InjectsRequestIDverifies end-to-end propagation usinglogz.WithRequestIDand anhttptest.Server.
Negative:
httpclienttakes a direct import dependency onlogz. This is accepted per ADR-001 (global) which permits direct imports between framework modules.- The header name
X-Request-IDis 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.