HTTP¶
The server, client and middleware now live in standalone modules
Everything below pkg/http's config adapters has moved out of GTB, and the earlier
re-export facade has been removed — import the owning module directly:
- HTTP server (
NewServer,Register,Start/Stop, health handlers,AuthMiddleware,SecurityHeadersMiddleware, lifecycle glue) →gitlab.com/phpboyscout/go/transport/http(aliastransporthttp). - Transport middleware (
NewChain,Chain,LoggingMiddleware,RateLimitMiddleware,OTelMiddleware, the client-middleware chain and itsWith…builders) →gitlab.com/phpboyscout/go/transit/http(aliastransithttp). - HTTP client factory (
NewClient,NewTransport, the clientWith…options) →gitlab.com/phpboyscout/go/httpclient(aliashttpclient).
pkg/http (alias gtbhttp) keeps only the config.Reader adapters — ServerSettingsFromConfig, ObserveServerSettingsFromConfig, NewServerFromReader, StartFromReader, RegisterFromReader, RateLimitConfigFromConfig, CircuitBreakerConfigFromConfig, with the GTB config-selection options WithConfigPrefix/WithPort. See the facades-removed migration note and the transport-extraction note. Behaviour is unchanged; only the import paths moved.
The pkg/http package provides hardened HTTP components for both server-side and client-side operations. It enforces secure TLS defaults, provides built-in observability endpoints, and mirrors the security posture required for production environments.
Server Control¶
The HTTP server implementation integrates seamlessly with the controls lifecycle management.
Features¶
- Standardized Endpoints: Automatically mounts
/healthz,/livez, and/readyz. - Production Timeouts: Pre-configured Read (5s), Write (10s), and Idle (120s) timeouts.
- Secure TLS: Enforces TLS 1.2 minimum with curated AEAD-based cipher suites and X25519 preference.
TLS Configuration¶
TLS configuration cascades — transport-specific keys override the shared defaults:
| Key | Shared Default | HTTP Override |
|---|---|---|
| Enabled | server.tls.enabled |
server.http.tls.enabled |
| Certificate | server.tls.cert |
server.http.tls.cert |
| Private key | server.tls.key |
server.http.tls.key |
To use the same certificate for both HTTP and gRPC, configure the shared keys only:
When TLS is enabled, the server uses ServeTLS with the shared hardened config from pkg/tls (TLS 1.2+, curated AEAD ciphers, X25519). When disabled, it uses plain Serve.
TLS configuration and resolution live in pkg/tls (gtbtls.DefaultConfig, gtbtls.Resolve, the typed gtbtls.Pair, plus the CertPool/ClientConfig client helpers), shared across the HTTP, gRPC and gateway transports.
Running a Second HTTP Server¶
The core server constructors take package-owned typed settings. GTB config is
adapted at the framework boundary with ServerSettingsFromConfig for one-shot
construction or ObserveServerSettingsFromConfig for reload-aware snapshots.
By default RegisterFromReader reads server settings from the server.http config prefix. To run more than one HTTP server in the same process — for example a public API server plus an internal/admin server — pass a ServerOption so each reads its own config block or binds an explicit port. RegisterFromReader builds the server, wires its start/stop into the controller, and returns it in one call:
view := props.Config.View() // pin one snapshot for the reads below
// Public API server on server.http.*
gtbhttp.RegisterFromReader(ctx, "public", controller, view, props.Logger, pubHandler)
// Internal admin server on its own config block (server.admin.* — port, tls.*,
// max_header_bytes), falling back to the shared server.port.
gtbhttp.RegisterFromReader(ctx, "admin", controller, view, props.Logger, admHandler,
gtbhttp.WithConfigPrefix("server.admin"))
// ...or a fixed port with no config block at all — WithPort overrides config:
gtbhttp.RegisterFromReader(ctx, "debug", controller, view, props.Logger, dbgHandler,
gtbhttp.WithPort(9090))
WithConfigPrefix threads the prefix through both construction and start, so the listen port and TLS settings stay consistent. When the resolved port is 0, the OS assigns an ephemeral port and start logs the actually-bound address.
Need the server without controller integration? NewServerFromReader + StartFromReader split the same config-driven flow, and for config-free construction from explicit typed settings the core constructors live in the transport module — transporthttp.NewServer(ctx, settings, handler), transporthttp.StartWithTLSPair(slogLog, srv, tlsPair), transporthttp.Stop(slogLog, srv). ServerSettingsFromConfig(view, prefix) resolves a transporthttp.ServerSettings from GTB config when you want to bridge the two.
Observing Server Settings¶
For long-lived GTB composition, bind the config section once and pass the observed snapshot through a package-owned settings source:
settings, err := gtbhttp.ObserveServerSettingsFromConfig(
props.Config,
"server.http",
config.WithSectionApply(func(change config.SectionChange[gtbhttp.ServerSettings]) error {
props.Logger.Info("http server settings changed", "version", change.Version)
return nil
}),
)
if err != nil {
return err
}
var source gtbhttp.ServerSettingsSource = settings
_ = source.Current()
ObserveServerSettingsFromConfig uses the same resolved config semantics as
ServerSettingsFromConfig, including the shared server.port fallback. The
binding rehydrates on successful config reloads, increments Version() only
when the typed ServerSettings snapshot changes, and exposes the latest
immutable value through Current(). Existing HTTP servers do not automatically
restart when the port changes; use the observed source for newly constructed
servers or explicit package-level reconfiguration logic.
Server Options¶
ServerOption values configure NewServer and Start, and are also accepted by Register:
WithConfigPrefix(prefix string) ServerOption: Config prefix for port, TLS and max-header-bytes (defaultserver.http).WithPort(port int) ServerOption: Explicit listen port, bypassing config lookup (highest precedence).WithMaxHeaderBytes(n int) ServerOption: Overrides<prefix>.max_header_bytesand the 1 MB default.WithReadTimeout/WithWriteTimeout/WithIdleTimeout(d time.Duration) ServerOption: Override the built-inhttp.Servertimeouts.WithServerTLSConfig(c *tls.Config) ServerOption: Replaces the default hardened*tls.Configon the constructed server (named distinctly from the client-sideWithTLSConfig).
Bind address, invalid ports & option safety¶
The server reads a bind address from <prefix>.host (e.g. server.http.host).
It defaults to "" — all interfaces (0.0.0.0 / [::]), unchanged from prior
releases — so set it to 127.0.0.1 to restrict a listener (admin, metrics) to
loopback. The transport also exposes transporthttp.WithHost / WithBindAddress.
See the bind-address migration note.
An out-of-range explicit port passed via WithPort (e.g. 70000) is now a
hard error from NewServerFromReader/RegisterFromReader, not a silent ephemeral
(:0) bind — it is forwarded to the transport's validated port resolution. Pass
WithPort(0) explicitly if you genuinely want an OS-assigned ephemeral port.
An unsupported option type (a value that is neither a GTB ServerOption nor a
transport ServerOption) is rejected rather than silently dropped:
NewServerFromReader returns an error naming the offending type; StartFromReader
(no error return) logs a WARN.
Functions¶
NewServer(ctx context.Context, settings ServerSettings, handler http.Handler, opts ...ServerOption) (*http.Server, error): Returns a pre-configured*http.Serverfrom typed settings.ServerSettingsFromConfig(cfg config.Reader, prefix string) ServerSettings: GTB adapter helper for one-shot settings resolution. Empty prefix defaults toserver.http.ObserveServerSettingsFromConfig(cfg config.Binder, prefix string, opts ...config.SectionBindingOption[ServerSettings]) (*config.ObservedSection[ServerSettings], error): GTB adapter helper for reload-aware typed server settings.StartWithTLSPair(logger *slog.Logger, srv *http.Server, tlsPair gtbtls.Pair) controls.StartFunc: Returns a controller start function. PassWithConfigPrefixto match a server built on a custom prefix; it logs the bound listener address (so an ephemeral:0port surfaces resolved).Register(ctx context.Context, id string, controller controls.Controllable, logger *slog.Logger, handler http.Handler, settings ServerSettings, tlsPair gtbtls.Pair, opts ...any) (*http.Server, error): Creates, configures, and registers the server with aController. The variadic accepts bothServerOptionandRegisterOptionvalues (mirroringpkg/grpc.Register). Health endpoints (/healthz,/livez,/readyz) are mounted outside any middleware chain.NewServerFromReader,StartFromReader,RegisterFromReader: GTB adapters that read the existing config structure through aconfig.Reader(typicallyprops.Config.View()) and delegate to the typed constructors.Stop(logger *slog.Logger, srv *http.Server) controls.StopFunc: Returns a controller stop function. It callssrv.Shutdown(ctx)to drain in-flight requests; if the shutdown context deadline expires (a handler outlives it), the server is force-closed viasrv.Close()so a hung handler cannot leave the listener and connections open. This mirrors the gRPC transport's graceful-then-force-stop behaviour.
Drain semantics: the server's per-request BaseContext is detached from the construction context with context.WithoutCancel. Cancelling the construction context (typically at shutdown) therefore does not cancel already-accepted requests mid-drain — Shutdown is left to drain them within its deadline. Context values on the construction context are still propagated to each request.
Middleware Chaining¶
The HTTP server chain is one of four transport middleware surfaces. For the cross-cutting pattern (server/client × HTTP/gRPC), the resilience composition rules, and the config-prefix convention, see the Transport Middleware & Resilience concept.
The alice-style middleware chaining API lives in gitlab.com/phpboyscout/go/transit/http (import as transithttp); the server-side security-headers and auth middleware live in gitlab.com/phpboyscout/go/transport/http (import as transporthttp), and WithMiddleware is a transporthttp RegisterOption. Middleware uses the standard func(http.Handler) http.Handler signature.
transithttp.NewChain(middlewares ...Middleware) Chain: Creates a middleware chain. The first middleware is the outermost wrapper.(c Chain) Append(middlewares ...Middleware) Chain: Returns a new chain with additional middleware appended (immutable).(c Chain) Extend(other Chain) Chain: Composes two chains.(c Chain) Then(handler http.Handler) http.Handler: Applies the chain to a handler.(c Chain) ThenFunc(fn http.HandlerFunc) http.Handler: Convenience forThen(http.HandlerFunc(fn)).WithMiddleware(chain Chain) RegisterOption: Applies a middleware chain when usingRegister. Health endpoints are unaffected.
Built-in Logging Middleware¶
LoggingMiddleware (in transithttp) logs each completed HTTP request with structured fields (method, path, status, latency, bytes, client IP, user agent).
transithttp.LoggingMiddleware(logger *slog.Logger, opts ...LoggingOption) Middleware
Format options (WithFormat):
| Format | Description |
|---|---|
FormatStructured (default) |
Structured key-value fields via *slog.Logger |
FormatCommon |
NCSA Common Log Format |
FormatCombined |
NCSA Combined Log Format (CLF + Referer + User-Agent) |
FormatJSON |
Single JSON object per request |
Other options: WithLogLevel, WithoutLatency, WithoutUserAgent, WithPathFilter, WithHeaderFields, WithTrustedProxy.
Client IP and trusted proxies: by default the logged client_ip is taken from the connection's RemoteAddr. The X-Forwarded-For and X-Real-IP headers are ignored because any direct client can forge them. Enable WithTrustedProxy() only when the server sits behind a trusted reverse proxy or load balancer that overwrites these headers; with it set, the left-most X-Forwarded-For entry (falling back to X-Real-IP) is used. Enabling it on a directly-exposed server lets clients spoof the recorded client IP.
Built-in Security-Headers Middleware¶
SecurityHeadersMiddleware (in transporthttp) sets a conservative set of response security headers on every request. It returns a transithttp.Middleware, so it composes into any transithttp.NewChain/transporthttp.WithMiddleware pipeline.
transporthttp.SecurityHeadersMiddleware(opts ...SecurityHeadersOption) transithttp.Middleware
Default headers:
| Header | Default value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Content-Security-Policy |
frame-ancestors 'none' |
Referrer-Policy |
no-referrer |
Strict-Transport-Security |
(off by default) |
Options:
| Option | Effect |
|---|---|
WithContentTypeOptions(v) |
Override X-Content-Type-Options (empty omits the header). |
WithFrameOptions(v) |
Override X-Frame-Options (empty omits the header). |
WithReferrerPolicy(v) |
Override Referrer-Policy (empty omits the header). |
WithContentSecurityPolicy(p) |
Set a full CSP, replacing the frame-ancestors-only default. An empty value falls back to the default so the clickjacking control is never silently dropped. |
WithHSTS(maxAge, includeSubdomains, preload) |
Enable Strict-Transport-Security. Off by default — HSTS is only meaningful (and only safe) over TLS. A non-positive maxAge leaves it disabled. |
Headers are set before the wrapped handler runs, so a handler that writes its own response still emits them; a handler may override any value by setting its own.
Applied to the built-in surfaces by default. The interactive docs/OpenAPI handlers (the standalone go/transport-openapi module's Register) and the documentation server (pkg/docs.Serve) wrap their handlers with this middleware automatically — the docs UI serves a "try-it" console that benefits from nosniff/frame/referrer protections. Customise via openapi.WithSecurityHeaderOptions(...) or opt out with openapi.WithoutSecurityHeaders(). The middleware is not forced onto user-supplied handlers; add it to your own chain where you want it.
Built-in Rate-Limit Middleware¶
RateLimitMiddleware protects a server from overload by admitting requests under a token-bucket limiter and rejecting excess traffic with 429 Too Many Requests plus a Retry-After header (which a GTB client's retry layer honours — a pleasing closed loop). It is an ordinary Middleware, so it composes into any Chain.
transithttp.RateLimitMiddleware(log *slog.Logger, cfg RateLimitConfig) Middlewaretransithttp.DefaultRateLimitConfig() RateLimitConfig— 50 rps sustained, burst 100, single global buckettransithttp.ClientIPKey(r *http.Request) string— a ready-made per-clientKeyFuncgtbhttp.RateLimitConfigFromConfig(cfg config.Reader, prefix string) transithttp.RateLimitConfig— GTB config adapter that reads policy from config (stays inpkg/http)
RateLimitConfig fields:
| Field | Default | Description |
|---|---|---|
RequestsPerSecond |
50 | Sustained token-bucket fill rate |
Burst |
100 | Bucket capacity — max instantaneous spike admitted |
KeyFunc |
nil | Derives a per-client key; nil = one global bucket |
MaxTrackedKeys |
8192 | Bound on the per-key bucket store (ignored when KeyFunc is nil) |
OnLimited |
nil | Optional callback invoked on each rejection (metrics/telemetry) |
Admission is non-blocking (Allow, not Wait): ingress must reject excess, never queue it, or a flood would exhaust memory.
Scoping is composition, not configuration. A global limiter is one entry in the server chain; a per-route limiter wraps a single handler; a per-client limiter sets KeyFunc. The per-key bucket store is bounded and LRU-evicting (capped at MaxTrackedKeys) so an attacker rotating source keys cannot exhaust memory.
Security note on
ClientIPKey. It keys on the connection'sRemoteAddrand deliberately ignoresX-Forwarded-For/X-Real-IP— those headers are spoofable by any direct client, so keying on them would let an attacker both evade their own bucket and churn the key store. A server behind a trusted reverse proxy that terminates XFF should supply its ownKeyFuncthat reads the proxy-set header.
Health endpoints (/healthz, /livez, /readyz) are mounted outside the WithMiddleware chain, so a global limiter never throttles liveness/readiness probes.
Config keys (RateLimitConfigFromConfig, prefix defaults to server.http):
| Key | Maps to |
|---|---|
server.http.ratelimit.requests_per_second |
RequestsPerSecond |
server.http.ratelimit.burst |
Burst |
server.http.ratelimit.max_tracked_keys |
MaxTrackedKeys |
Unset keys keep their defaults; the code-only fields (KeyFunc, OnLimited) are never read from config — wiring stays explicit.
Built-in Authentication Middleware¶
AuthMiddleware authenticates (and optionally authorizes) each request from an go/authn verifier — an API key, a JWT/OIDC bearer token, a session cookie, or an mTLS client certificate — storing the verified identity in the request context. It is an ordinary Middleware, so it composes into any Chain.
AuthMiddleware and its options live in transporthttp.
transporthttp.AuthMiddleware(opts ...AuthOption) (transithttp.Middleware, error)— with no verifier configured it is a construction error (fail-closed)transporthttp.IdentityFromContext(ctx context.Context) (*authn.Identity, bool)— read the verified identity in a handler (same context key as the gRPC interceptor)
keys, _ := authn.NewAPIKeyVerifier(authn.KeyEntry{Key: ciKey, Subject: "ci"})
authMW, _ := transporthttp.AuthMiddleware(
transporthttp.WithAPIKeyHeader("X-API-Key", keys),
transporthttp.WithAuthorize(authn.RequireScopes("api:write")),
transporthttp.WithAuthLogger(logger.ToSlog(props.Logger)),
)
chain := transithttp.NewChain(transithttp.LoggingMiddleware(logger.ToSlog(props.Logger)), authMW)
Options: WithBearerVerifier, WithAPIKeyHeader, WithCookieVerifier, WithMTLSVerifier, WithAuthorize, WithAuthLogger, WithAuthSkipper. On failure it writes a generic 401 (with WWW-Authenticate) or 403 and logs the cause with the credential redacted — never disclosing why to the client. Health endpoints are outside the chain, so a global auth middleware never gates probes. See Authentication & Authorization for the full reference, including the verifiers, the authorization seam, credential precedence, and the security model.
WithCookieVerifier(name, v) reads the credential from a named cookie. The cookie is an ambient credential — the browser sends it on every request, including <img>/<audio>/<video> sub-resource loads that cannot set an Authorization header — so it sits below the explicit header schemes in precedence (an explicit bearer or API-key header always wins; the cookie is consulted only when no header credential is presented). This makes a browser session authenticate sub-resources while leaving explicit API clients unaffected; typically paired with a token-in-URL bootstrap that sets the cookie on first load (Jupyter-style).
Usage Example¶
mux := http.NewServeMux()
mux.HandleFunc("/api/data", myDataHandler)
// Build a middleware chain (transit/http)
chain := transithttp.NewChain(
transithttp.LoggingMiddleware(logger.ToSlog(props.Logger),
transithttp.WithFormat(transithttp.FormatCombined),
transithttp.WithPathFilter("/healthz", "/livez", "/readyz"),
),
)
// Build settings via the GTB config adapter, then register on the transport server.
// Health endpoints stay outside the chain.
view := props.Config.View()
settings := gtbhttp.ServerSettingsFromConfig(view, "server.http")
tlsPair := gtbtls.Resolve(view, "server.http.tls")
srv, err := transporthttp.Register(ctx, "http-api", controller, logger.ToSlog(props.Logger), mux, settings, tlsPair,
transporthttp.WithMiddleware(chain),
)
Client Factory¶
The hardened http.Client factory for outbound requests lives in the standalone, framework-free module gitlab.com/phpboyscout/go/httpclient (v0.1.0), imported as httpclient.
The re-export facade has been removed: import
gitlab.com/phpboyscout/go/httpclientdirectly forNewClient,NewTransportand the clientWith…options. The client-middleware chain,RetryConfigand the circuit breaker live alongside the server middleware ingitlab.com/phpboyscout/go/transit/http(transithttp);httpclient.WithClientMiddleware/WithRetrytake thosetransithttptypes.RateLimitConfigFromConfig/CircuitBreakerConfigFromConfigremain GTB config adapters inpkg/http. See the facades-removed and httpclient-extraction notes.
Features¶
- Mandatory Timeouts: Default 30s timeout to prevent blocked goroutines.
- Secure Transport: Uses the same hardened TLS configuration as the server.
- Scheme Protection: Redirect policy rejects HTTPS-to-HTTP downgrades.
- Connection Limits: Pre-configured idle connection pooling and timeouts.
Functions¶
httpclient.NewClient(opts ...ClientOption) *http.Client: Returns a hardened HTTP client.httpclient.NewTransport(tlsCfg *tls.Config) *http.Transport: Returns a pre-configured secure transport for custom client needs.
Options¶
All httpclient ClientOption values:
httpclient.WithTimeout(d time.Duration)httpclient.WithMaxRedirects(n int)httpclient.WithTLSConfig(cfg *tls.Config)httpclient.WithCertPool(pool *x509.CertPool)— trusts a custom root CA pool (private CA / self-signed) while preserving the hardened TLS defaults; build the pool withtls.CertPoolhttpclient.WithTransport(rt http.RoundTripper)httpclient.WithRetry(cfg transithttp.RetryConfig)— enables automatic retry with exponential backoff (RetryConfigis atransithttptype)httpclient.WithClientMiddleware(chain transithttp.ClientChain)— applies atransithttpmiddleware chain to the transport
Retry with Exponential Backoff¶
The client supports opt-in retry for transient failures via WithRetry. Retry is implemented as a http.RoundTripper decorator, so it composes cleanly with custom transports set via WithTransport.
RetryConfig fields:
| Field | Default | Description |
|---|---|---|
MaxRetries |
3 | Maximum number of retry attempts (0 = no retries) |
InitialBackoff |
500ms | Base delay before the first retry |
MaxBackoff |
30s | Cap on computed delay |
RetryableStatusCodes |
429, 502, 503, 504 | HTTP status codes that trigger a retry |
ShouldRetry |
nil | Optional custom predicate replacing default logic |
Backoff strategy: Full jitter — uniform random in [0, min(cap, base × 2^attempt)]. This reduces thundering-herd effects compared to fixed or equal-jitter backoff. The exponential term is clamped to MaxBackoff before the doubling so the computed delay can never overflow. Invalid RetryConfig values (negative MaxRetries, non-positive backoff durations, MaxBackoff below InitialBackoff) are normalised to safe defaults at client construction.
Client Middleware Chain¶
The client supports composable RoundTripper middleware, mirroring the server-side chain pattern. The chain type and its built-in middleware live in transithttp; httpclient.WithClientMiddleware applies the assembled chain. Middleware wraps the transport — the first in the chain executes first on the request and last on the response.
// transithttp.ClientMiddleware wraps an http.RoundTripper with additional behaviour.
type ClientMiddleware func(next http.RoundTripper) http.RoundTripper
Chain API (transithttp):
transithttp.NewClientChain(middlewares ...ClientMiddleware) ClientChain— creates a chain(c ClientChain) Append(middlewares ...ClientMiddleware) ClientChain— returns a new chain with additional middleware (immutable)(c ClientChain) Then(rt http.RoundTripper) http.RoundTripper— applies the chain to a transport
Built-in middleware:
| Middleware | Description |
|---|---|
WithRequestLogging(log) |
Logs method, URL, status code, and duration at debug level. Headers and body are NOT logged (security). |
WithBearerToken(token) |
Injects Authorization: Bearer {token}. Sent only to the first host the client addresses, so a cross-host redirect cannot capture the token. |
WithBasicAuth(user, pass) |
Injects Authorization: Basic {base64}. Host-pinned like WithBearerToken — not re-sent across a cross-host redirect. |
WithRateLimit(rps) |
Token bucket rate limiting. Blocks until a token is available or the request context is cancelled. |
Usage example:
chain := transithttp.NewClientChain(
transithttp.WithRequestLogging(logger.ToSlog(props.Logger)),
transithttp.WithBearerToken(os.Getenv("API_TOKEN")),
transithttp.WithRateLimit(10), // 10 requests per second
)
client := httpclient.NewClient(
httpclient.WithTimeout(30 * time.Second),
httpclient.WithClientMiddleware(chain),
)
The middleware chain is applied after retry wrapping, so retry operates on the raw transport (not on logged/authed requests), and a ClientMiddleware placed in the chain therefore sits outside the retry transport. Custom middleware can be written by implementing the ClientMiddleware function signature.
Circuit Breaker¶
WithCircuitBreaker is a ClientMiddleware that fails fast while a downstream is consistently failing, avoiding wasted retry/backoff cycles against a service that will not answer. It is the partner primitive to retry: retry handles transient flakiness, the breaker handles a hard outage.
transithttp.WithCircuitBreaker(log *slog.Logger, cfg CircuitBreakerConfig) ClientMiddlewaretransithttp.DefaultCircuitBreakerConfig() CircuitBreakerConfig— threshold 5, cooldown 30s, half-open trial 1transithttp.ErrCircuitOpen— sentinel error returned (wrapped) while open; test witherrors.Isgtbhttp.CircuitBreakerConfigFromConfig(cfg config.Reader, prefix string) transithttp.CircuitBreakerConfig— GTB config adapter (stays inpkg/http)
States: StateClosed (admit all, count failures) → StateOpen (reject immediately with ErrCircuitOpen until the cooldown elapses) → StateHalfOpen (admit a bounded number of trials; the first success closes the breaker, any failure re-opens it).
CircuitBreakerConfig fields:
| Field | Default | Description |
|---|---|---|
FailureThreshold |
5 | Consecutive failures (in Closed) that trip the breaker open |
Cooldown |
30s | How long the breaker stays Open before a trial |
HalfOpenMaxRequests |
1 | Trial requests admitted in HalfOpen |
IsFailure |
nil | Classifier; default = transport errors + 5xx are failures |
OnStateChange |
nil | Optional transition callback (metrics/telemetry) |
By default, transport errors and 5xx responses count as failures; 4xx do not — in particular a 429 does not trip the breaker (that is the retry/backoff layer's concern, not a downstream-health signal).
Ordering (composition with retry). Place the breaker in the ClientChain so it sits outside the retry transport:
The breaker sees the final post-retry verdict: one logical call that exhausts its retry budget against a dead service counts as one breaker failure, not N. Once Open, calls are rejected before entering the retry layer — so no backoff sleeps are spent on a service known to be down (exactly the waste the retry design flagged). The breaker never serves a cached response; an open breaker returns an error, never a stored body.
client := httpclient.NewClient(
httpclient.WithRetry(transithttp.DefaultRetryConfig()),
httpclient.WithClientMiddleware(transithttp.NewClientChain(
transithttp.WithCircuitBreaker(logger.ToSlog(props.Logger), transithttp.DefaultCircuitBreakerConfig()),
transithttp.WithRequestLogging(logger.ToSlog(props.Logger)),
)),
)
Config keys (CircuitBreakerConfigFromConfig, prefix defaults to server.http): server.http.circuitbreaker.failure_threshold, .cooldown, .half_open_max_requests.
Retry-After support: When a 429 or 503 response includes a Retry-After header (seconds or HTTP-date), that value is used as the delay instead of the computed backoff. The header value is clamped to MaxBackoff, so a hostile or misconfigured server cannot stall the client beyond the configured cap.
Body rewind: Request bodies are rewound via GetBody between attempts. Response bodies from failed attempts are drained and closed to allow connection reuse. A request whose body has already been consumed and that has no GetBody to rewind it (e.g. a streamed io.Reader body) is not retried — resending it would silently send an empty or partial body. A nil body (e.g. GET) is always safe to retry.
Context cancellation: If the request context is cancelled during a backoff wait, the retry loop exits immediately with the context error.
Usage Example¶
// Simple secure client
client := httpclient.NewClient()
// Client with automatic retry for transient failures
client := httpclient.NewClient(
httpclient.WithTimeout(60*time.Second),
httpclient.WithRetry(transithttp.DefaultRetryConfig()),
)
// Custom retry configuration
client := httpclient.NewClient(
httpclient.WithRetry(transithttp.RetryConfig{
MaxRetries: 5,
InitialBackoff: 200 * time.Millisecond,
MaxBackoff: 10 * time.Second,
RetryableStatusCodes: []int{429, 502, 503, 504},
}),
)
// Custom retry predicate
client := httpclient.NewClient(
httpclient.WithRetry(transithttp.RetryConfig{
MaxRetries: 3,
InitialBackoff: 500 * time.Millisecond,
MaxBackoff: 30 * time.Second,
ShouldRetry: func(attempt int, resp *http.Response, err error) bool {
if err != nil {
return true // retry all errors
}
return resp != nil && resp.StatusCode >= 500
},
}),
)
// Power user: custom client with secure transport (stdlib *http.Client + hardened transport)
customClient := &http.Client{
Transport: httpclient.NewTransport(nil),
}