Skip to content

Compose client middleware

httpclient provides the hardened *http.Client; the per-request behaviour — retry, circuit breaking, credentials, logging — comes from go/transit's client round-trippers. This guide shows how the two fit together and in what order.

import (
    "gitlab.com/phpboyscout/go/httpclient"
    transithttp "gitlab.com/phpboyscout/go/transit/http"
)

The two wiring options

NewClient takes two middleware-related options:

Option Wraps Use for
WithRetry(cfg) the transport directly, closest to the wire automatic retry of transient failures
WithClientMiddleware(chain) the transport outside retry everything else: circuit breaker, auth, logging, rate limit

Both take go/transit types. Retry is applied closest to the raw transport; the ClientChain wraps the result, so a chain member (a circuit breaker, a logger) sees one logical call rather than each retry attempt.

A full client stack

client := httpclient.NewClient(
    httpclient.WithTimeout(10*time.Second),

    // retry sits closest to the transport
    httpclient.WithRetry(transithttp.DefaultRetryConfig()),

    // the chain wraps retry — breaker outermost so it sees the post-retry verdict
    httpclient.WithClientMiddleware(transithttp.NewClientChain(
        transithttp.WithCircuitBreaker(log, transithttp.DefaultCircuitBreakerConfig()),
        transithttp.WithBearerToken(token),
        transithttp.WithRequestLogging(log),
    )),
)

This produces the layering breaker → auth → logging → retry → transport. The transit middleware model explains why the breaker belongs outside retry.

Available chain middleware

From go/transit/http, for use inside NewClientChain:

  • WithCircuitBreaker(log, cfg) — fail fast while a downstream is unhealthy.
  • WithBearerToken(token) / WithBasicAuth(user, pass) — credentials, pinned to the first host so a redirect cannot capture them.
  • WithRateLimit(rps) — throttle outbound requests.
  • WithRequestLogging(log) — debug-log each request.

Bring your own transport

WithTransport(rt) replaces the hardened transport entirely (retry and the chain still wrap it). WithTLSConfig/WithCertPool adjust TLS while keeping the hardened defaults. See hardened defaults.