Documentation
¶
Overview ¶
Package httpretrier provides configurable retry execution for outbound HTTP requests.
Problem ¶
Remote dependencies fail in transient ways (timeouts, connection resets, temporary 5xx/429 responses). Without a consistent retry strategy, callers either fail too aggressively or duplicate ad hoc retry loops with different policies across services.
Solution ¶
This package wraps an HTTPClient with retry orchestration driven by a pluggable RetryIfFn.
HTTPRetrier.Do executes a request up to a bounded number of attempts, applying delay growth and jitter between retries. The retry decision function receives both `*http.Response` and `error`, enabling policy decisions based on transport failures and/or HTTP status codes.
Built-in Retry Policies ¶
Predefined helpers are provided for common semantics:
- RetryIfForReadRequests for idempotent reads (e.g. GET)
- RetryIfForWriteRequests for state-changing writes (e.g. POST/PUT/PATCH)
- RetryIfFnByHTTPMethod to select one of the above from method name
The default policy retries only when `err != nil`.
Backoff Behavior ¶
Delay progression is configurable via:
- total attempts cap (WithAttempts)
- initial delay (WithDelay)
- multiplicative delay factor (WithDelayFactor)
- random jitter ceiling (WithJitter)
- maximum delay ceiling (WithMaxDelay)
- jitter strategy (WithJitterStrategy)
This produces bounded exponential-style backoff with randomization, helping reduce synchronized retry storms. Optionally, WithRespectRetryAfter makes the retrier wait at least the server-provided Retry-After delay, and WithOnRetry exposes each scheduled retry for logging or metrics.
Request Body Replay ¶
When a request has a body and retries are needed, the retrier relies on `Request.GetBody` to recreate the body stream for subsequent attempts. If the request has a body that cannot be recreated (GetBody missing or failing), retries cannot continue and Do returns an error. Bodyless requests (e.g. a GET with no body) retry without restriction.
Benefits ¶
httpretrier standardizes retry behavior for HTTP clients, improving resilience while keeping retry policies explicit, testable, and reusable across services.
Index ¶
- Constants
- Variables
- func RetryIfForReadRequests(r *http.Response, err error) bool
- func RetryIfForWriteRequests(r *http.Response, err error) bool
- type HTTPClient
- type HTTPRetrier
- type JitterStrategy
- type OnRetryFn
- type Option
- func WithAttempts(attempts uint) Option
- func WithDelay(delay time.Duration) Option
- func WithDelayFactor(delayFactor float64) Option
- func WithJitter(jitter time.Duration) Option
- func WithJitterStrategy(strategy JitterStrategy) Option
- func WithMaxDelay(maxDelay time.Duration) Option
- func WithMaxRetryAfter(maxRetryAfter time.Duration) Option
- func WithOnRetry(onRetry OnRetryFn) Option
- func WithRespectRetryAfter() Option
- func WithRetryIfFn(retryIfFn RetryIfFn) Option
- type RetryIfFn
Constants ¶
const ( // DefaultAttempts is the default maximum number of total attempts (the // initial request plus retries). DefaultAttempts = 4 // DefaultDelay is the delay to apply after the first failed attempt. DefaultDelay = 1 * time.Second // DefaultDelayFactor is the default multiplication factor to get the successive delay value. DefaultDelayFactor = 2 // DefaultJitter is the maximum random Jitter time between retries. DefaultJitter = 100 * time.Millisecond // DefaultMaxDelay is the default upper bound for the computed backoff delay // (before jitter). It prevents the exponential growth from overflowing. DefaultMaxDelay = 30 * time.Second // DefaultMaxRetryAfter is the default cap applied to a server-provided // Retry-After delay (see [WithRespectRetryAfter] and [WithMaxRetryAfter]). DefaultMaxRetryAfter = 24 * time.Hour )
const ( JitterAdditive = backoff.JitterAdditive // fixed additive ceiling (default) JitterFull = backoff.JitterFull // rand[0, delay): best decorrelation JitterEqual = backoff.JitterEqual // delay/2 + rand[0, delay/2) )
Jitter strategies, re-exported from backoff so callers need not import it.
Variables ¶
var ErrBodyNotReplayable = errors.New("cannot retry: the request body has already been consumed and Request.GetBody is not set")
ErrBodyNotReplayable is returned by HTTPRetrier.Do when a retry is required but the request body has already been consumed and Request.GetBody is not available to recreate it.
Functions ¶
func RetryIfForReadRequests ¶
RetryIfForReadRequests is a retry policy for idempotent requests: retries on 404/408/409/423/425/429/500/502/503/504/507 or transport error. It deliberately retries some codes that are often terminal (404/409/423): for a read this targets read-after-write eventual consistency, where the resource may materialize on a retry. Use a custom RetryIfFn if that is not desired.
Types ¶
type HTTPClient ¶
type HTTPClient interface {
// Do performs the HTTP request. As with [net/http.Client.Do], a non-nil error
// implies a nil response and vice versa.
Do(req *http.Request) (*http.Response, error)
}
HTTPClient is the minimal client contract used by HTTPRetrier.
type HTTPRetrier ¶
type HTTPRetrier struct {
// contains filtered or unexported fields
}
HTTPRetrier applies configurable retry policies to HTTP requests.
An HTTPRetrier holds only immutable configuration once constructed, so a single instance can be safely shared across goroutines and used for concurrent or overlapping HTTPRetrier.Do calls. All per-call mutable state is kept in a separate [doState] value created inside Do.
func New ¶
func New(httpClient HTTPClient, opts ...Option) (*HTTPRetrier, error)
New constructs HTTP retrier wrapping client with exponential backoff retry orchestration from provided options.
func (*HTTPRetrier) Do ¶
Do executes request up to configured attempts with exponential delay and jitter between retries, applying retry decision function to each response.
Do is safe for concurrent use of a single HTTPRetrier instance: every call keeps its own mutable state in a local [doState]. Each concurrent call must use its own *http.Request, since a retry mutates the request's body.
An already-canceled request context fails fast. If the context is canceled just as a retry timer fires, one further attempt may run (with the canceled request) before Do returns the context error.
type JitterStrategy ¶
type JitterStrategy = backoff.JitterStrategy
JitterStrategy selects how backoff jitter is applied. See backoff.JitterStrategy.
type OnRetryFn ¶
OnRetryFn is an optional observability callback invoked before each scheduled retry. It receives the number of the attempt that just failed (1-based), the delay before the next attempt, and the response/error that triggered the retry (the response body is already closed). It counts scheduled retries — one may still be preempted by cancellation before it runs — and must not panic; it runs inline in HTTPRetrier.Do.
type Option ¶
type Option func(c *HTTPRetrier) error
Option configures an HTTPRetrier instance.
func WithAttempts ¶
WithAttempts sets the maximum number of total attempts — the initial request plus retries (default 4); must be at least 1.
func WithDelay ¶
WithDelay sets initial delay between first failed attempt and first retry (default 1 second).
func WithDelayFactor ¶
WithDelayFactor sets exponential backoff multiplier (default 2): next_delay = current_delay * delayFactor.
func WithJitter ¶
WithJitter sets random jitter ceiling to prevent synchronized retry storms (default 100ms).
func WithJitterStrategy ¶
func WithJitterStrategy(strategy JitterStrategy) Option
WithJitterStrategy selects how backoff jitter is applied (default JitterAdditive). JitterFull and JitterEqual scale jitter with the delay for better decorrelation. Returns error if the strategy is not a defined JitterStrategy.
func WithMaxDelay ¶
WithMaxDelay sets the upper bound for the computed exponential backoff delay (before jitter), preventing unbounded growth and overflow (default 30s).
func WithMaxRetryAfter ¶
WithMaxRetryAfter caps the delay honored from a Retry-After header (default DefaultMaxRetryAfter, 24h). Only relevant with WithRespectRetryAfter. Returns error if maxRetryAfter < 1 nanosecond.
func WithOnRetry ¶
WithOnRetry registers an observability callback invoked before each scheduled retry (see OnRetryFn). Returns error if the callback is nil.
func WithRespectRetryAfter ¶
func WithRespectRetryAfter() Option
WithRespectRetryAfter makes the retrier honor a response's Retry-After header (delta-seconds or HTTP-date), waiting at least that long (plus jitter) before the next attempt, capped at WithMaxRetryAfter (default DefaultMaxRetryAfter). This can exceed WithMaxDelay, which bounds only the exponential backoff; a request context deadline is the ultimate bound on any wait. Default: disabled.
func WithRetryIfFn ¶
WithRetryIfFn customizes retry decision function (default: retry only on transport errors).
type RetryIfFn ¶
RetryIfFn decides whether a request should be retried after a response/error. It must not panic; it runs inline in HTTPRetrier.Do, before the response body is closed, so a panic would also leak that response.
func RetryIfFnByHTTPMethod ¶
RetryIfFnByHTTPMethod selects retry policy by HTTP method: idempotent read policy for GET, write policy for all others.