http

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// StateClosed admits all requests; failures are counted.
	StateClosed = CircuitState(resilience.StateClosed)
	// StateOpen rejects all requests immediately with ErrCircuitOpen until the
	// cooldown elapses, then transitions to StateHalfOpen.
	StateOpen = CircuitState(resilience.StateOpen)
	// StateHalfOpen admits a limited number of trial requests; success closes
	// the breaker, failure re-opens it.
	StateHalfOpen = CircuitState(resilience.StateHalfOpen)
)

The public states are derived from the shared core so the two enumerations can never silently drift out of order.

Variables

View Source
var ErrCircuitOpen = errors.New("http: circuit breaker is open")

ErrCircuitOpen is returned by the breaker when it is open. Callers may test for it with errors.Is. It is returned directly (not stack-wrapped per call): an open breaker is an expected control-flow signal on a high-volume reject path, not an exceptional error needing a fresh stack each time.

Functions

func ClientIPKey

func ClientIPKey(r *http.Request) string

ClientIPKey is a ready-made RateLimitConfig.KeyFunc that keys on the client IP using the connection's RemoteAddr.

It deliberately does NOT trust X-Forwarded-For / X-Real-IP: those headers are spoofable by any direct client, so keying a limiter on them would let an attacker both evade their own bucket and churn the bounded key store by rotating fake IPs. A server behind a trusted reverse proxy that terminates XFF should supply its own KeyFunc that reads the proxy-set header. This reuses the logging middleware's client-IP derivation with trustedProxy=false, the same safe default.

func NewRetryTransport

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

NewRetryTransport wraps next with automatic retry using exponential backoff and full jitter for transient failures. The config is normalised so invalid values are clamped to safe defaults. Client constructors expose this as an option (e.g. a WithRetry(RetryConfig)); transit provides the transport itself.

Types

type Chain

type Chain struct {
	// contains filtered or unexported fields
}

Chain composes zero or more Middleware into a single Middleware. Middleware is applied left-to-right: the first middleware in the list is the outermost wrapper (first to see the request, last to see the response).

chain := NewChain(recovery, logging, auth)
handler := chain.Then(mux)

func NewChain

func NewChain(middlewares ...Middleware) Chain

NewChain creates a new middleware chain from the given middleware functions. Nil entries are silently skipped.

func (Chain) Append

func (c Chain) Append(middlewares ...Middleware) Chain

Append returns a new Chain with additional middleware appended. The original chain is not modified. Nil entries are silently skipped.

func (Chain) Extend

func (c Chain) Extend(other Chain) Chain

Extend returns a new Chain that applies c's middleware first, then other's.

func (Chain) Then

func (c Chain) Then(handler http.Handler) http.Handler

Then applies the middleware chain to the given handler and returns the resulting http.Handler.

If handler is nil, http.DefaultServeMux is used.

func (Chain) ThenFunc

func (c Chain) ThenFunc(fn http.HandlerFunc) http.Handler

ThenFunc is a convenience for Then(http.HandlerFunc(fn)).

type CircuitBreakerConfig

type CircuitBreakerConfig struct {
	// FailureThreshold is the number of consecutive failures (within Closed)
	// that trips the breaker open. Must be >= 1. Default: 5.
	FailureThreshold int `mapstructure:"failure_threshold" yaml:"failure_threshold" json:"failure_threshold"`

	// Cooldown is how long the breaker stays Open before allowing a trial.
	// Default: 30s.
	Cooldown time.Duration `mapstructure:"cooldown" yaml:"cooldown" json:"cooldown"`

	// HalfOpenMaxRequests is the number of trial requests allowed in HalfOpen.
	// The first success closes the breaker; any failure re-opens it.
	// Must be >= 1. Default: 1.
	HalfOpenMaxRequests int `mapstructure:"half_open_max_requests" yaml:"half_open_max_requests" json:"half_open_max_requests"`

	// IsFailure classifies a round-trip outcome as a failure for breaker
	// accounting. When nil, the default treats transport errors and 5xx
	// responses (>=500) as failures; 4xx and 2xx/3xx are successes. A 429
	// (client rate-limited) therefore does NOT trip the breaker — that is
	// retry's job, not the breaker's.
	IsFailure func(resp *http.Response, err error) bool `mapstructure:"-" yaml:"-" json:"-"`

	// OnStateChange is invoked on every state transition. Optional; transitions
	// are also logged via the constructor's logger.
	OnStateChange func(from, to CircuitState) `mapstructure:"-" yaml:"-" json:"-"`
}

CircuitBreakerConfig configures the client-side circuit breaker.

func DefaultCircuitBreakerConfig

func DefaultCircuitBreakerConfig() CircuitBreakerConfig

DefaultCircuitBreakerConfig returns: threshold 5, cooldown 30s, half-open trial 1, default 5xx/transport-error failure classification.

func MergeCircuitBreakerConfig

func MergeCircuitBreakerConfig(base, override CircuitBreakerConfig, fields CircuitBreakerConfigOverrides) CircuitBreakerConfig

MergeCircuitBreakerConfig applies explicitly supplied typed override values to base while leaving code-only function fields under caller control.

type CircuitBreakerConfigOverrides

type CircuitBreakerConfigOverrides struct {
	FailureThreshold    bool
	Cooldown            bool
	HalfOpenMaxRequests bool
}

CircuitBreakerConfigOverrides records which typed circuit breaker config fields were explicitly supplied by an adapter.

type CircuitState

type CircuitState int

CircuitState is the client circuit breaker's state.

func (CircuitState) String

func (s CircuitState) String() string

String renders the state for logging.

type ClientChain

type ClientChain struct {
	// contains filtered or unexported fields
}

ClientChain composes ClientMiddleware in order. Immutable — Append returns a new chain.

func NewClientChain

func NewClientChain(middlewares ...ClientMiddleware) ClientChain

NewClientChain creates a ClientChain from the given middleware.

func (ClientChain) Append

func (c ClientChain) Append(middlewares ...ClientMiddleware) ClientChain

Append returns a new chain with additional middleware appended.

func (ClientChain) Then

Then applies the middleware chain to the given RoundTripper and returns the wrapped result.

type ClientMiddleware

type ClientMiddleware func(next http.RoundTripper) http.RoundTripper

ClientMiddleware wraps an http.RoundTripper with additional behaviour. The first middleware in a chain is the outermost wrapper — it executes first on the request and last on the response.

func WithBasicAuth

func WithBasicAuth(username, password string) ClientMiddleware

WithBasicAuth returns middleware that injects an Authorization: Basic header. The header is only sent to the first host the client addresses, so a cross-host redirect cannot capture the credential.

func WithBearerToken

func WithBearerToken(token string) ClientMiddleware

WithBearerToken returns middleware that injects an Authorization: Bearer header. The header is only sent to the first host the client addresses, so a cross-host redirect cannot capture the token.

func WithCircuitBreaker

func WithCircuitBreaker(log *slog.Logger, cfg CircuitBreakerConfig) ClientMiddleware

WithCircuitBreaker returns a ClientMiddleware that fails fast while a downstream is consistently failing, avoiding wasted retry/backoff cycles.

Place it OUTSIDE the retry transport — i.e. in the ClientChain via WithClientMiddleware, which wraps the transport after retry — so the breaker sees the final post-retry verdict: one retry-exhausted logical call counts as a single breaker failure, not one per attempt. Once Open, calls are rejected before entering the retry layer, so no backoff sleeps are spent on a service known to be down.

func WithRateLimit

func WithRateLimit(requestsPerSecond float64) ClientMiddleware

WithRateLimit returns middleware that limits outbound requests to the specified rate using a token-bucket limiter (burst 1). Blocks until a token is available or the request context is cancelled. The limiter is shared across all requests through the transport, so it holds under concurrency — the previous hand-rolled version let concurrent goroutines sleep in parallel and then proceed together, admitting a burst per interval.

func WithRequestLogging

func WithRequestLogging(log *slog.Logger) ClientMiddleware

WithRequestLogging returns middleware that logs each outbound request and response at debug level. Logs method, URL, status code, and duration. Headers and body are NOT logged for security.

type LogFormat

type LogFormat int

LogFormat controls the output format of the logging middleware.

const (
	// FormatStructured emits structured key-value fields via *slog.Logger.
	FormatStructured LogFormat = iota

	// FormatCommon emits NCSA Common Log Format (CLF).
	FormatCommon

	// FormatCombined emits NCSA Combined Log Format (CLF + Referer + User-Agent).
	FormatCombined

	// FormatJSON emits a single JSON object per request.
	FormatJSON
)

type LoggingOption

type LoggingOption func(*loggingConfig)

LoggingOption configures transport logging behaviour.

func WithFormat

func WithFormat(format LogFormat) LoggingOption

WithFormat sets the log output format. Defaults to FormatStructured.

func WithHeaderFields

func WithHeaderFields(headers ...string) LoggingOption

WithHeaderFields logs the specified request header values as fields. Header names are normalised to lowercase. Values are truncated to 256 bytes.

Known-sensitive headers (Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token, X-Csrf-Token, X-Session-Token, Proxy-Authorization) are always redacted regardless of whether they appear in the fields list. This is defence-in-depth against accidental credential leakage.

func WithLogLevel

func WithLogLevel(level slog.Level) LoggingOption

WithLogLevel sets the log level for successful requests. Defaults to slog.LevelInfo. Errors always log at slog.LevelError.

func WithPathFilter

func WithPathFilter(paths ...string) LoggingOption

WithPathFilter excludes requests matching the given paths from logging.

func WithTrustedProxy

func WithTrustedProxy() LoggingOption

WithTrustedProxy makes the logging middleware trust the client-supplied X-Forwarded-For and X-Real-IP headers when deriving the logged client IP.

These headers are trivially spoofable by any direct client, so they are IGNORED by default and the connection's RemoteAddr is logged instead. Enable this option only when the server sits behind a trusted reverse proxy or load balancer that overwrites (rather than appends to) these headers. Enabling it on a directly-exposed server lets clients forge the recorded client IP.

func WithoutLatency

func WithoutLatency() LoggingOption

WithoutLatency disables the "latency" field.

func WithoutUserAgent

func WithoutUserAgent() LoggingOption

WithoutUserAgent disables the "user_agent" field.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is the standard Go HTTP middleware signature.

func LoggingMiddleware

func LoggingMiddleware(l *slog.Logger, opts ...LoggingOption) Middleware

LoggingMiddleware returns an HTTP Middleware that logs each completed request.

func OTelMiddleware

func OTelMiddleware(server string, opts ...otelhttp.Option) Middleware

OTelMiddleware returns a Middleware that records an OpenTelemetry server span and the standard server metrics (http.server.*) for each request, reading whichever TracerProvider and MeterProvider are installed as the OTel globals (see telemetry.Setup). server names the span operation, identifying this service in the trace.

It composes in a Chain like any other middleware. Put it ahead of the logging middleware so the request log can pick up the active span:

chain := http.NewChain(
    http.OTelMiddleware("macguffin"),
    http.LoggingMiddleware(log),
)

func RateLimitMiddleware

func RateLimitMiddleware(log *slog.Logger, cfg RateLimitConfig) Middleware

RateLimitMiddleware returns a Middleware that admits requests under a token-bucket limiter and rejects excess traffic with 429 Too Many Requests plus a Retry-After header (which a GTB client's retry layer honours). An invalid config is clamped to defaults rather than rejected.

Because it is an ordinary Middleware it composes into any Chain and can be scoped globally (one entry in the server chain) or per-route (wrap a single handler). Per-client limiting is enabled by setting RateLimitConfig.KeyFunc.

Health endpoints (/healthz, /livez, /readyz) are mounted outside the WithMiddleware chain by Register, so a global limiter never throttles probes.

type RateLimitConfig

type RateLimitConfig struct {
	// RequestsPerSecond is the sustained fill rate of the token bucket.
	// Must be > 0. Default: 50.
	RequestsPerSecond float64 `mapstructure:"requests_per_second" yaml:"requests_per_second" json:"requests_per_second"`

	// Burst is the bucket capacity — the maximum number of requests that may
	// be admitted in an instantaneous spike. Must be >= 1. Default: 100.
	Burst int `mapstructure:"burst" yaml:"burst" json:"burst"`

	// KeyFunc derives the limiter key for a request, enabling per-client
	// limiting. When nil, a single global bucket is used for all requests.
	// A common choice is to key on the client IP (see ClientIPKey).
	KeyFunc func(*http.Request) string `mapstructure:"-" yaml:"-" json:"-"`

	// MaxTrackedKeys bounds the per-client bucket store. Ignored when KeyFunc
	// is nil. Must be >= 1. Default: 8192.
	MaxTrackedKeys int `mapstructure:"max_tracked_keys" yaml:"max_tracked_keys" json:"max_tracked_keys"`

	// OnLimited is invoked when a request is rejected, before the 429 is
	// written. Optional; useful for metrics/telemetry. A structured debug log
	// is emitted via the constructor's logger regardless.
	OnLimited func(*http.Request) `mapstructure:"-" yaml:"-" json:"-"`
}

RateLimitConfig configures the server-side token-bucket rate limiter.

func DefaultRateLimitConfig

func DefaultRateLimitConfig() RateLimitConfig

DefaultRateLimitConfig returns a RateLimitConfig suitable for a modest management/API server: 50 rps sustained, burst 100, single global bucket.

func MergeRateLimitConfig

func MergeRateLimitConfig(base, override RateLimitConfig, fields RateLimitConfigOverrides) RateLimitConfig

MergeRateLimitConfig applies explicitly supplied typed override values to base while leaving code-only function fields under caller control.

type RateLimitConfigOverrides

type RateLimitConfigOverrides struct {
	RequestsPerSecond bool
	Burst             bool
	MaxTrackedKeys    bool
}

RateLimitConfigOverrides records which typed rate limit config fields were explicitly supplied by an adapter.

type RetryConfig

type RetryConfig struct {
	// MaxRetries is the maximum number of retry attempts. Zero means no retries.
	MaxRetries int
	// InitialBackoff is the base delay before the first retry. Default: 500ms.
	InitialBackoff time.Duration
	// MaxBackoff caps the computed delay. Default: 30s.
	MaxBackoff time.Duration
	// RetryableStatusCodes defines which HTTP status codes trigger a retry.
	// Default: []int{429, 502, 503, 504}.
	RetryableStatusCodes []int
	// ShouldRetry is an optional custom predicate. When set, it replaces the
	// default status-code and network-error checks. The attempt count (0-based)
	// and either the response or the transport error are provided.
	ShouldRetry func(attempt int, resp *http.Response, err error) bool
}

RetryConfig configures the retry behaviour of the HTTP client.

func DefaultRetryConfig

func DefaultRetryConfig() RetryConfig

DefaultRetryConfig returns a RetryConfig suitable for most use cases.

Jump to

Keyboard shortcuts

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