httpclient

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package httpclient provides outbound HTTP for Credo applications: timeout, retry with backoff, structured logging, and W3C trace context propagation — composed as an http.RoundTripper chain on a plain *http.Client.

The package is stdlib-only: it imports neither the root credo package nor any third-party library, so it can be used in any Go program. The logger is a plain *slog.Logger (in app code, typically infra.Logger).

Quick Start

client := httpclient.New(
	httpclient.WithTimeout(10*time.Second),
	httpclient.WithRetry(),
	httpclient.WithLogging(infra.Logger),
	httpclient.WithTracePropagation(),
)

New returns a *http.Client — anything that accepts one (SDKs, generated API clients, stdlib helpers) works unchanged. Register it via plain DI:

app.ProvideValue(client)

or wrap it in a named type when an application needs several clients.

Chain Order

Options compose in a canonical order regardless of the order they are passed: Client.Timeout (total budget) → retry → logging → trace → base transport. Retry sits outermost, so every attempt gets its own log line and its own traceparent span ID. The individual transports are exported (NewRetryTransport, NewLoggingTransport, NewTraceTransport) for composing onto an existing client.

Safe-by-Default Retry

DefaultRetryIf retries idempotent methods only (GET, HEAD, OPTIONS, TRACE, PUT, DELETE) on transport errors and 5xx responses — POST is never retried by default, and a request body is only replayed when req.GetBody is set. See NewRetryTransport.

Trace Propagation

Tracing here is manual W3C traceparent header propagation — enough for log/trace correlation across services without an OTel SDK. Forward the inbound request's trace context to outbound calls with TraceContextFromRequest and SetTraceContext. OTel propagators, span creation, and request metrics land with the observability phase.

Maturity: beta

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultRetryIf

func DefaultRetryIf(req *http.Request, resp *http.Response, err error) bool

DefaultRetryIf reports whether a request should be retried: idempotent method AND (transport error OR 5xx response), never after context cancellation or deadline expiry.

Idempotent methods are GET, HEAD, OPTIONS, TRACE, PUT, and DELETE. Anything else — POST in particular — is never retried by default: retrying a non-idempotent request can duplicate side effects (double payments). Callers with idempotency keys can opt in via RetryConfig.RetryIf. A 429 response is not retried either; override RetryIf to change that.

func New

func New(opts ...Option) *http.Client

New returns a plain *http.Client with the configured RoundTripper chain — everything that accepts an http.Client works unchanged.

Options compose in a canonical order regardless of the order they are passed:

http.Client.Timeout              ← total budget, incl. all retries
└── retry                        ← outermost transport: drives attempts
    └── logging                  ← one line per attempt
        └── trace                ← fresh traceparent per attempt
            └── base transport   ← cloned http.DefaultTransport (or WithBaseTransport)

Retry sits outermost so each attempt re-enters logging and trace: every attempt gets its own log line and its own span ID.

New() with no options is equivalent to an http.Client with a cloned default transport and no timeout.

func NewLoggingTransport

func NewLoggingTransport(base http.RoundTripper, logger *slog.Logger) http.RoundTripper

NewLoggingTransport wraps base with structured outbound logging: one slog line per attempt with method, URL (query string and userinfo stripped — they routinely carry secrets), status or transport error, duration, the attempt number when the retry transport is active, and the trace ID when the request context carries one (see SetTraceContext).

Levels follow Credo's access-log convention: Error for transport errors and 5xx, Warn for 4xx, Info otherwise. Headers and bodies are never logged.

Panics if logger is nil (config misuse). A nil base defaults to http.DefaultTransport.

func NewRetryTransport

func NewRetryTransport(base http.RoundTripper, cfg ...RetryConfig) http.RoundTripper

NewRetryTransport wraps base with retry-with-backoff behavior. Zero args use DefaultRetryConfig; zero-valued config fields fall back to their defaults.

Backoff between attempts is full-jitter exponential: a uniformly random duration in [0, min(MaxDelay, MinDelay·2^(attempt-1))). Waits abort immediately when the request context is done, returning the context's error.

A request with a body is retried only when req.GetBody is set (the stdlib sets it automatically for *bytes.Buffer, *bytes.Reader, and *strings.Reader bodies). Without GetBody, the first response or error is returned as-is — a request is never silently re-sent with a half-consumed body.

When attempts are exhausted, the last response (or error) is returned unchanged: a final 503 arrives as (resp, nil), exactly like the stdlib. The response body of a discarded attempt is drained (up to a small cap) and closed so the underlying connection can be reused.

A nil base defaults to http.DefaultTransport.

func NewTraceTransport

func NewTraceTransport(base http.RoundTripper) http.RoundTripper

NewTraceTransport wraps base with W3C trace context propagation. For each request:

  • an already-set traceparent header is left untouched;
  • a valid TraceContext in the request context (see SetTraceContext) yields a child traceparent — same trace ID and flags, fresh random span ID — with tracestate forwarded unchanged;
  • otherwise a new root traceparent is generated (random trace and span IDs, flags 01). Invalid inbound values are discarded per the W3C restart guidance.

The derived span IDs do not correspond to recorded spans — there is no tracer here. The point is trace-ID continuity, so logs and downstream tracers correlate across services.

A nil base defaults to http.DefaultTransport.

func SetTraceContext

func SetTraceContext(ctx context.Context, tc TraceContext) context.Context

SetTraceContext returns a copy of ctx carrying tc. The trace transport (see WithTracePropagation) derives a child traceparent from it for every outbound attempt made with the returned context.

Types

type Option

type Option func(*options)

Option configures New.

func WithBaseTransport

func WithBaseTransport(rt http.RoundTripper) Option

WithBaseTransport sets the innermost transport that performs the actual HTTP exchange. Default: a clone of http.DefaultTransport.

func WithLogging

func WithLogging(logger *slog.Logger) Option

WithLogging enables structured outbound logging through the given logger (in app code, typically infra.Logger). See NewLoggingTransport for the attributes and level mapping. Panics if logger is nil (config misuse).

func WithRetry

func WithRetry(cfg ...RetryConfig) Option

WithRetry enables retry with full-jitter exponential backoff. Zero args use DefaultRetryConfig. See NewRetryTransport for the semantics.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets http.Client.Timeout — the total budget for a call, including all retry attempts and backoff waits. Per-call deadlines come from the request context.

func WithTracePropagation

func WithTracePropagation() Option

WithTracePropagation enables W3C traceparent injection on outbound requests. See NewTraceTransport for the derivation rules and TraceContextFromRequest / SetTraceContext for the server-side wiring.

type RetryConfig

type RetryConfig struct {
	// MaxAttempts is the total number of attempts including the first.
	// Default 3 (one initial attempt plus two retries).
	MaxAttempts int

	// MinDelay is the backoff ceiling for the first retry. Default 100ms.
	MinDelay time.Duration

	// MaxDelay caps the backoff ceiling. Default 2s.
	MaxDelay time.Duration

	// RetryIf reports whether a request should be retried after the given
	// response or transport error (exactly one of resp/err is non-nil).
	// It receives the caller's original request — implementations must not
	// read its Body, which has already been consumed by the first attempt.
	// Default [DefaultRetryIf].
	RetryIf func(req *http.Request, resp *http.Response, err error) bool
}

RetryConfig configures the retry transport (see WithRetry and NewRetryTransport). Zero-valued fields fall back to the corresponding DefaultRetryConfig values.

func DefaultRetryConfig

func DefaultRetryConfig() RetryConfig

DefaultRetryConfig returns the configuration used by WithRetry and NewRetryTransport when no config is supplied. Each call returns a fresh value, so callers cannot mutate the package-wide defaults.

type TraceContext

type TraceContext struct {
	TraceParent string
	TraceState  string
}

TraceContext carries W3C Trace Context headers from an inbound server request to outbound client calls. It is a plain string carrier — no OTel types are involved.

The TraceParent format is "00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>" (W3C Trace Context version 00). TraceState is optional vendor data, forwarded unchanged.

func GetTraceContext

func GetTraceContext(ctx context.Context) (TraceContext, bool)

GetTraceContext returns the TraceContext attached to ctx via SetTraceContext, reporting whether one is present.

func TraceContextFromRequest

func TraceContextFromRequest(r *http.Request) (TraceContext, bool)

TraceContextFromRequest reads the W3C traceparent/tracestate headers from an inbound request. It reports false when no traceparent header is present. The value is not validated here — invalid values are discarded at injection time, per the W3C restart guidance.

Typical server-side wiring:

callCtx := ctx.Context()
if tc, ok := httpclient.TraceContextFromRequest(ctx.Request().Request); ok {
	callCtx = httpclient.SetTraceContext(callCtx, tc)
}
req, err := http.NewRequestWithContext(callCtx, http.MethodGet, url, nil)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL