Documentation
¶
Overview ¶
Package httpx provides a resilient outbound-HTTP toolkit: transient-error classification, generic retry with jittered exponential backoff, Retry-After parsing, HTTP status mapping, secret redaction, body draining, custom-CA TLS transports, and a configurable redirect allowlist.
Index ¶
- Constants
- Variables
- func CATransport(pem []byte) (*http.Transport, error)
- func CheckHTTPStatus(resp *http.Response) error
- func CloneDefaultTransport() (*http.Transport, error)
- func Close(c *http.Client)
- func DefaultRedirectPolicy(req *http.Request, via []*http.Request) error
- func DockerGitHubRedirectPolicy(req *http.Request, via []*http.Request) error
- func Drain(body io.ReadCloser)
- func DrainClose(rc io.ReadCloser)
- func IsPermanent(err error) bool
- func IsTransient(err error) bool
- func JitteredBackoff(backoff time.Duration) time.Duration
- func LimitedBody(resp *http.Response, limit int64) io.ReadCloser
- func LogSafeError(err error) error
- func NewClient(timeout time.Duration) *http.Client
- func ParseRetryAfter(h string) time.Duration
- func ParseRetryAfterResponse(resp *http.Response) time.Duration
- func Permanent(err error) error
- func ReadLimitedBody(body io.ReadCloser, limit int64) ([]byte, error)
- func RedactSecret(err error, secret string) error
- func RedactSecretString(s, secret string) string
- func RedactTransportError(err error, prefix, secret string) error
- func RedirectPolicyFunc(opts ...RedirectOption) func(*http.Request, []*http.Request) error
- func RefuseAllRedirects(*http.Request, []*http.Request) error
- func Retry(ctx context.Context, client *http.Client, reqURL string, opts ...Option) ([]byte, error)
- func RetryOnRateLimit(ctx context.Context, maxAttempts int, maxWait time.Duration, ...) error
- func RetryWithBackoff[T any](ctx context.Context, maxAttempts int, baseDelay time.Duration, label string, ...) (T, error)
- func SafeDouble(d time.Duration) time.Duration
- func SleepCtx(ctx context.Context, d time.Duration) error
- type AuthError
- type Backoff
- type CheckRetry
- type ConditionalResult
- type ExpBackoffOption
- type ExponentialBackoff
- type HTTPStatusError
- type OnRetry
- type Option
- type PermanentError
- type PrepareRetry
- type RTOption
- func WithBackoffFunc(f func() Backoff) RTOption
- func WithCheckRetry(cr CheckRetry) RTOption
- func WithOnRetry(fn OnRetry) RTOption
- func WithPrepareRetry(fn PrepareRetry) RTOption
- func WithRTBaseDelay(d time.Duration) RTOption
- func WithRTMaxAttempts(n int) RTOption
- func WithRTMaxElapsedTime(d time.Duration) RTOption
- func WithRetryNonIdempotent(enable bool) RTOption
- type RateLimitError
- type RedirectOption
- type ResponseTooLargeError
- type RetryAfterHint
- type RetryRoundTripper
- type StatusError
- type Transient
- type Validators
Examples ¶
Constants ¶
const ( // DefaultBaseDelay is the production base for exponential-backoff retry. DefaultBaseDelay = time.Second // DefaultMaxAttempts caps Retry at three tries. DefaultMaxAttempts = 3 // DefaultMaxBodyBytes caps response bodies at 10 MB. DefaultMaxBodyBytes int64 = 10 << 20 // RetryAfterCap is the maximum Retry-After honor duration. RetryAfterCap = 60 * time.Second )
const BackoffStop time.Duration = -1
BackoffStop signals that no more retries should be made.
Variables ¶
var ErrNoCertsInPEM = errors.New("httpx: no PEM-encoded certificates found")
ErrNoCertsInPEM is returned by CATransport when the supplied PEM data contains no parseable certificates. A misconfigured or empty CA file therefore fails loudly rather than silently producing a transport that trusts nothing (which would reject every connection with an opaque error).
var ErrRateLimited = errors.New("rate limited")
ErrRateLimited is a sentinel callers use with errors.Is to detect 429 responses.
var ErrServerError = errors.New("server error")
ErrServerError is a sentinel for upstream 5xx responses.
Functions ¶
func CATransport ¶ added in v2.1.0
CATransport builds an *http.Transport that trusts ONLY the CA certificate(s) in pem for TLS verification (pinning). It is cloned from http.DefaultTransport (via CloneDefaultTransport), so it keeps the standard connection pooling, dial/keepalive timeouts, HTTP/2 negotiation, and proxy support (ProxyFromEnvironment). A FRESH TLS config is then installed — RootCAs from pem, a TLS 1.2 minimum, and verification always ENABLED (InsecureSkipVerify is not set). Any TLS settings already on http.DefaultTransport are intentionally NOT carried over, so the returned transport's trust posture cannot be weakened by a program that globally mutated the default transport's *tls.Config (e.g. set InsecureSkipVerify or an accept-all VerifyPeerCertificate hook).
The supplied CA(s) are the SOLE trust anchors: the transport rejects any host not chaining to them, including public-CA hosts. This is the right setup for a known self-hosted endpoint presenting a private or self-signed certificate (a Plex server, an internal API).
The returned *http.Transport is concrete and mutable, so callers may tune fields such as MaxIdleConnsPerHost or pass it as the base RoundTripper of NewRetryRoundTripper:
tr, err := httpx.CATransport(pem) client := httpx.NewRetryRoundTripper(tr, httpx.WithRTMaxAttempts(3)).StandardClient()
It returns ErrNoCertsInPEM when pem yields no certificates. The caller owns reading the PEM bytes (from a file, a secret, an env var), which keeps this function I/O-free and lets the caller bound the read as it sees fit.
CATransport requires http.DefaultTransport to be the standard library's *http.Transport (the default). If your program has replaced it with a wrapping RoundTripper (for example request instrumentation), CATransport returns an error.
func CheckHTTPStatus ¶
CheckHTTPStatus maps HTTP error status codes to typed errors. Returns nil for 2xx/3xx. 401/403 → *AuthError, 429 → *RateLimitError, others ≥400 → *HTTPStatusError.
func CloneDefaultTransport ¶ added in v2.5.0
CloneDefaultTransport returns a private clone of http.DefaultTransport, keeping the standard connection pooling, dial/keepalive timeouts, HTTP/2 negotiation, and proxy support (ProxyFromEnvironment). The clone is the caller's to mutate — set a per-attempt ResponseHeaderTimeout, tune MaxIdleConnsPerHost, or pass it as the base RoundTripper of NewRetryRoundTripper — without the global footgun of mutating http.DefaultTransport itself, which would silently reconfigure every other client in the process.
It returns an error when http.DefaultTransport has been replaced by a non-*http.Transport RoundTripper (request instrumentation, a test stub): a wrapping RoundTripper offers no concrete transport to clone, and failing loudly beats silently dropping the wrapper's behavior. It is the building block CATransport is assembled on.
func DefaultRedirectPolicy ¶
DefaultRedirectPolicy is the default redirect policy: it allows a redirect only to the same host as the original request, and refuses a same-host https->http scheme downgrade (which would forward a custom auth header onto a cleartext hop). A cross-host redirect is refused (Go forwards a custom header across a redirect, so it would leak) and an http->https upgrade is allowed. Equivalent to RedirectPolicyFunc(WithSameHost()); use RedirectPolicyFunc for a custom allowlist, a higher hop cap (WithMaxHops), or to permit downgrades (WithAllowSchemeDowngrade).
func DockerGitHubRedirectPolicy ¶
DockerGitHubRedirectPolicy is an OPTIONAL example redirect policy allowing docker.com and github.com hosts. Use it by assigning to Client.CheckRedirect or pass RedirectOption values to RedirectPolicyFunc for other allowlists.
func Drain ¶
func Drain(body io.ReadCloser)
Drain reads and discards up to 64 KB of a response body to enable HTTP connection reuse.
func DrainClose ¶
func DrainClose(rc io.ReadCloser)
DrainClose reads remaining bytes (up to drainLimit) from rc before closing it.
func IsPermanent ¶
IsPermanent reports whether err (or any wrapped error) is a *PermanentError.
func IsTransient ¶
IsTransient returns true for errors likely caused by temporary server or network issues worth retrying. Auth, rate-limit, permanent, and context errors are never transient.
func JitteredBackoff ¶
JitteredBackoff returns a duration in [backoff/2, backoff] using the "equal jitter" strategy (per AWS Builders' Library). Full jitter and decorrelated jitter are intentionally not provided — equal jitter is the recommended default for HTTP retry as it avoids thundering herd while maintaining a minimum backoff floor.
func LimitedBody ¶
func LimitedBody(resp *http.Response, limit int64) io.ReadCloser
LimitedBody wraps resp.Body with an io.LimitReader capped at limit bytes, preserving the original Close method.
func LogSafeError ¶ added in v2.5.0
LogSafeError returns an error whose message is safe to log. A transport *url.Error embeds the full request URL (with any userinfo/query secrets), so it is reduced to its underlying cause. Nil returns nil; *StatusError already renders a redacted URL via Error(), so it (and everything else) passes through unchanged — preserving errors.Is/As chains for callers.
httpx applies this reduction to every transport error it logs or wraps; it is exported so a caller wrapping transport errors into its own messages can apply the same one (equivalent to RedactTransportError(err, "", "") — reach for that variant when a known secret must also be scrubbed from the text).
func NewClient ¶
NewClient returns an *http.Client with the given timeout and the DefaultRedirectPolicy (same-host only). For custom redirect allowlists, configure CheckRedirect with RedirectPolicyFunc or assign DockerGitHubRedirectPolicy.
func ParseRetryAfter ¶
ParseRetryAfter parses a Retry-After header value (delta-seconds or HTTP-date). Returns zero for missing/malformed values. Caps at RetryAfterCap for safety (prevents unbounded waits in retry loops). For raw uncapped values, use ParseRetryAfterResponse.
func ParseRetryAfterResponse ¶
ParseRetryAfterResponse parses the Retry-After header from an *http.Response. Returns zero if absent or unparseable. Does NOT cap — preserves the raw duration so callers (e.g., CheckHTTPStatus) can make their own decisions. For capped values suitable for retry loops, use ParseRetryAfter.
func Permanent ¶
Permanent wraps err to indicate it should never be retried. Mirrors cenkalti/backoff.Permanent().
func ReadLimitedBody ¶ added in v2.3.0
func ReadLimitedBody(body io.ReadCloser, limit int64) ([]byte, error)
ReadLimitedBody reads body up to limit bytes, always closes body, and returns the bytes read. It reads one byte past limit to detect an over-limit body and returns *ResponseTooLargeError (with nil bytes) rather than a silently truncated payload — a truncated body indistinguishable from a complete one is a corruption hazard. A limit of math.MaxInt64 means "effectively unlimited" and is guarded against probe-size overflow.
It is the read-all-with-overflow-detection companion to LimitedBody (which only caps the stream and leaves reading and overflow handling to the caller), and is the same cap+1 read Retry applies internally — exposed for callers that issue their own request and decode outside Retry but still want the fail-loud size bound. On any error the body is already closed.
func RedactSecret ¶
RedactSecret replaces occurrences of secret in err's message with "REDACTED".
func RedactSecretString ¶ added in v2.3.0
RedactSecretString replaces every occurrence of secret in s with "REDACTED" and returns the result. It is the string-level building block behind RedactSecret and RedactTransportError, exposed for callers that must redact a secret from a plain string — a captured HTTP response body destined for an error field or a log line — rather than from an error value. An empty secret is a no-op (s is returned unchanged), matching the error-shaped variants.
func RedactTransportError ¶
RedactTransportError unwraps *url.Error and redacts the secret from the error message. Returns nil for nil input.
func RedirectPolicyFunc ¶
RedirectPolicyFunc returns a CheckRedirect function configured with the given options. A redirect is followed only when its target host is allowed — an exact WithAllowedHosts entry, a WithAllowedSuffixes match, or (with WithSameHost) the original request's own host — and, unless WithAllowSchemeDowngrade is set, the redirect does not downgrade https->http. With no allowlist and no WithSameHost, all redirects are refused. The hop cap is WithMaxHops (default 5).
func RefuseAllRedirects ¶ added in v2.5.0
RefuseAllRedirects is a CheckRedirect policy that follows NO redirect: it returns http.ErrUseLastResponse, so the client hands the caller the redirect response itself (status 3xx, body open, nil error) instead of the followed hop. It is the policy for a token-bearing client of an API that issues no redirects: Go's client forwards custom request headers (an X-Plex-Token, an X-Api-Key) across redirects — only Authorization, Cookie, and WWW-Authenticate are stripped, and only on a cross-domain hop — so a hostile 302 (MITM, DNS poisoning) would exfiltrate the credential to an attacker-chosen origin. With the hop refused, the credential never leaves the configured host and the unexpected 3xx surfaces to the caller's own status handling.
func Retry ¶
Retry performs an HTTP GET with bounded exponential-backoff retry on 429 and 5xx responses and on transient transport errors (timeouts, connection resets, DNS failures - see IsTransient). 4xx (non-429) and non-transient transport errors are returned immediately. Honors Retry-After (capped at RetryAfterCap).
Retry deliberately keeps its own retry loop rather than delegating to RetryRoundTripper.RoundTrip. It is a decorator over the same shared primitives (JitteredBackoff, SafeDouble, SleepCtx, ParseRetryAfter, IsTransient, Drain), not a thin wrapper over the RoundTripper cycle, because Retry carries behavior the transparent RoundTripper has no equivalent for and which must stay byte-for-byte stable for existing consumers:
- []byte return with the body capped at cfg.maxBodyBytes (the RoundTripper hands back an *http.Response and never reads the body);
- URL/secret redaction on every log "url" attr (redactURL) and every returned/wrapped error (LogSafeError, StatusError.Error()), the CWE-532 hardening the RoundTripper path does not perform;
- rich per-attempt slog logging plus the "retries exhausted after %s: %w" wrapper, which the RoundTripper exposes only as an OnRetry hook;
- classification of every 5xx (not just 502/503/504) as retryable and of any non-2xx (3xx included) as a permanent *StatusError. A 2xx response returns the body; Retry cannot surface a redirect, so 3xx is an error.
Routing Retry through RoundTrip would silently change one or more of these, so the loop is intentionally not merged.
Example ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"github.com/cplieger/httpx/v2"
)
func main() {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, "hello")
}))
defer srv.Close()
body, err := httpx.Retry(context.Background(), srv.Client(), srv.URL,
httpx.WithMaxAttempts(3),
httpx.WithBaseDelay(100*time.Millisecond),
)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(string(body))
}
Output: hello
func RetryOnRateLimit ¶
func RetryOnRateLimit(ctx context.Context, maxAttempts int, maxWait time.Duration, fn func(ctx context.Context) error) error
RetryOnRateLimit calls fn up to maxAttempts times (total, including the first call) when it returns a *RateLimitError. Non-rate-limit errors are returned immediately. maxAttempts is the TOTAL attempt count; a value below 1 is treated as 1, so fn is always called at least once. The context is passed to fn on each attempt.
func RetryWithBackoff ¶
func RetryWithBackoff[T any](ctx context.Context, maxAttempts int, baseDelay time.Duration, label string, fn func(ctx context.Context) (T, error), ) (T, error)
RetryWithBackoff calls fn up to maxAttempts times (total, including the first call) with jittered exponential backoff, returning the first success. Non-transient errors are returned immediately. maxAttempts is the TOTAL attempt count, not retries-beyond-first; a value below 1 is treated as 1, so fn is always called at least once (it never silently no-ops). Logging uses slog.Default() and cannot be overridden per-call; control output via slog.SetDefault().
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/cplieger/httpx/v2"
)
func main() {
attempts := 0
result, err := httpx.RetryWithBackoff(context.Background(), 3, time.Millisecond, "example", func(_ context.Context) (string, error) {
attempts++
if attempts < 2 {
return "", &httpx.HTTPStatusError{Code: 503}
}
return "done", nil
})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(result)
}
Output: done
func SafeDouble ¶
SafeDouble doubles a duration, guarding against int64 overflow.
Types ¶
type AuthError ¶
type AuthError struct{ Msg string }
AuthError indicates invalid or expired credentials.
type Backoff ¶
type Backoff interface {
// NextBackOff returns the next wait duration, or BackoffStop to stop.
NextBackOff() time.Duration
// Reset restores the strategy to its initial state.
Reset()
}
Backoff is a pluggable backoff strategy. NextBackOff returns the duration to wait before the next retry. Return BackoffStop to signal no more retries. Mirrors cenkalti/backoff.BackOff.
type CheckRetry ¶
CheckRetry is the signature for a retry policy function. It receives the context, the response (may be nil on transport error), and the error (nil on successful response). It returns whether to retry and an optional error to short-circuit with. Mirrors hashicorp/go-retryablehttp CheckRetry.
type ConditionalResult ¶ added in v2.6.0
type ConditionalResult struct {
// Validators are the fresh validators captured from a 200 response's ETag /
// Last-Modified headers (either may be empty when the server sent none).
// Zero on a 304: the caller keeps the validators it already holds.
Validators Validators
// Body is the full response body of a 200, bounded by the maxBodyBytes
// given to DoConditional. Nil on a 304.
Body []byte
// NotModified reports a 304: the cached representation is still current.
NotModified bool
}
ConditionalResult is one conditional-request outcome. Fields are ordered largest-alignment-first for govet fieldalignment.
func DoConditional ¶ added in v2.6.0
func DoConditional(client *http.Client, req *http.Request, v Validators, maxBodyBytes int64) (ConditionalResult, error)
DoConditional executes req as a single conditional request: it sets If-None-Match / If-Modified-Since from v (an empty field is not sent), performs the request on client, and classifies the response.
- 304 -> NotModified=true (body drained and closed; Validators zero — keep the ones you sent).
- 200 -> the bounded body plus the response's fresh validators. A body over maxBodyBytes fails loud with *ResponseTooLargeError rather than being silently truncated; maxBodyBytes <= 0 means DefaultMaxBodyBytes.
- Anything else -> an error: the CheckHTTPStatus mapping for >= 400 (*AuthError, *RateLimitError — non-transient; *HTTPStatusError — transient for 502/503/504), or a plain non-transient error for a status that is neither usable content nor a revalidation (a 204, a 3xx from a redirect-refusing client). The body is always closed.
It is deliberately a SINGLE attempt so the caller owns the retry and cache policy: wrap it in RetryWithBackoff (transient classification composes through the returned errors), rebuild req per attempt, and decide app-side when a cached copy may be reused on failure (stale-on-error) and whether validators may be sent at all (send the zero Validators when the cached body is unusable, so an empty cache can never be "revalidated" into a 304 with nothing to reuse). Intended for GET (or HEAD, where Body stays empty).
type ExpBackoffOption ¶
type ExpBackoffOption func(*expBackoffCfg)
ExpBackoffOption configures an ExponentialBackoff.
func WithInitialInterval ¶
func WithInitialInterval(d time.Duration) ExpBackoffOption
WithInitialInterval sets the first backoff duration. Default: DefaultBaseDelay.
func WithMaxElapsedTime ¶
func WithMaxElapsedTime(d time.Duration) ExpBackoffOption
WithMaxElapsedTime caps total retry time for the backoff. Zero means no cap.
type ExponentialBackoff ¶
type ExponentialBackoff struct {
// contains filtered or unexported fields
}
ExponentialBackoff implements Backoff with jittered exponential backoff. This is the default strategy used throughout httpx.
func NewExponentialBackoff ¶
func NewExponentialBackoff(opts ...ExpBackoffOption) *ExponentialBackoff
NewExponentialBackoff creates an ExponentialBackoff with functional options.
func (*ExponentialBackoff) NextBackOff ¶
func (b *ExponentialBackoff) NextBackOff() time.Duration
NextBackOff returns the next jittered backoff duration, or BackoffStop if MaxElapsedTime has already elapsed. The check is made BEFORE the returned interval is slept, so the caller's total time can overshoot MaxElapsedTime by up to one interval (the final returned wait). When a hard ceiling is required, use the RoundTripper's WithRTMaxElapsedTime, which aborts on elapsed+wait and never overshoots.
func (*ExponentialBackoff) Reset ¶
func (b *ExponentialBackoff) Reset()
Reset restores the backoff to its initial state.
type HTTPStatusError ¶
type HTTPStatusError struct {
Code int
}
HTTPStatusError represents a non-2xx HTTP response not covered by AuthError or RateLimitError. Implements the Transient interface for 502/503/504.
func (*HTTPStatusError) Error ¶
func (e *HTTPStatusError) Error() string
func (*HTTPStatusError) IsClientError ¶
func (e *HTTPStatusError) IsClientError() bool
IsClientError reports whether the status code is 4xx.
func (*HTTPStatusError) IsServerError ¶
func (e *HTTPStatusError) IsServerError() bool
IsServerError reports whether the status code is 5xx.
func (*HTTPStatusError) IsTransient ¶
func (e *HTTPStatusError) IsTransient() bool
IsTransient reports whether the status code is a retryable server failure (502/503/504).
type OnRetry ¶
OnRetry is called before each retry attempt (not the initial request). attempt is 1-indexed (first retry = 1). resp may be nil on transport errors.
type Option ¶
type Option func(*retryCfg)
Option configures a Retry call.
func WithBaseDelay ¶
WithBaseDelay sets the initial backoff delay. Default: DefaultBaseDelay (1s).
func WithHeaders ¶
WithHeaders sets a function that is called to set headers on each request.
func WithLogger ¶
WithLogger sets the logger for retry diagnostics. Default: slog.Default().
func WithMaxAttempts ¶
WithMaxAttempts sets the maximum number of attempts (including the first). Default: DefaultMaxAttempts (3).
func WithMaxBodyBytes ¶
WithMaxBodyBytes sets the maximum response body size to read. Default: DefaultMaxBodyBytes (10 MB).
type PermanentError ¶
type PermanentError struct {
Err error
}
PermanentError wraps an error to signal that it should NOT be retried, regardless of other retry policies. Mirrors cenkalti/backoff.PermanentError. Use Permanent(err) to wrap.
func (*PermanentError) Error ¶
func (e *PermanentError) Error() string
func (*PermanentError) Is ¶
func (e *PermanentError) Is(target error) bool
Is allows errors.Is matching against other PermanentErrors.
func (*PermanentError) Unwrap ¶
func (e *PermanentError) Unwrap() error
type PrepareRetry ¶
PrepareRetry is called before each retry to allow request mutation (e.g., re-signing with a fresh token). Mirrors go-retryablehttp PrepareRetry.
type RTOption ¶
type RTOption func(*rtCfg)
RTOption configures a RetryRoundTripper via NewRetryRoundTripper.
func WithBackoffFunc ¶
WithBackoffFunc sets a factory that returns a fresh custom backoff strategy for each request. When set, the round-tripper's base delay is ignored.
The factory is invoked once per RoundTrip, so every request drives its own independent Backoff instance. This is required for correctness under the documented shared-transport pattern (one StandardClient fanned across goroutines): a single long-lived Backoff would have its progression rewound and advanced concurrently by unrelated requests. Return a new instance (e.g. NewExponentialBackoff(...)) from the factory; the fresh instance needs no Reset.
func WithCheckRetry ¶
func WithCheckRetry(cr CheckRetry) RTOption
WithCheckRetry sets a custom retry policy. If nil, the default policy retries on transient transport errors and 429/502/503/504 responses.
func WithOnRetry ¶
WithOnRetry sets a hook called before each retry attempt for observability.
func WithPrepareRetry ¶
func WithPrepareRetry(fn PrepareRetry) RTOption
WithPrepareRetry sets a hook called before each retry to mutate the request (e.g., refresh auth tokens).
func WithRTBaseDelay ¶
WithRTBaseDelay sets the initial backoff delay for the round-tripper (used when no custom Backoff is provided). Default: DefaultBaseDelay (1s).
func WithRTMaxAttempts ¶
WithRTMaxAttempts sets the maximum number of attempts (TOTAL, including the initial request). Default: DefaultMaxAttempts (3). A value below 1 is treated as 1, so the request is always sent at least once. This counts total attempts, not retries-beyond-first.
func WithRTMaxElapsedTime ¶
WithRTMaxElapsedTime caps total time spent retrying. Zero means no cap.
func WithRetryNonIdempotent ¶
WithRetryNonIdempotent enables retry of non-idempotent methods (POST, PUT, PATCH, DELETE) when the request has a GetBody function for body replay.
type RateLimitError ¶
RateLimitError indicates a rate limit was exceeded. RetryAfter, when non-zero, is the RAW, UNCAPPED hint from the upstream's Retry-After header (populated via ParseRetryAfterResponse). The upstream controls this value; a hostile or misconfigured server can supply a very large duration (CWE-400 uncontrolled resource consumption). Callers that sleep on it directly MUST bound it first, e.g. min(err.RetryAfter, cap). RetryOnRateLimit already does this (caps at its maxWait argument). For a pre-capped value use ParseRetryAfter (bounded at RetryAfterCap = 60s).
func (*RateLimitError) Error ¶
func (e *RateLimitError) Error() string
type RedirectOption ¶
type RedirectOption func(*redirectCfg)
RedirectOption configures a redirect policy created by RedirectPolicyFunc.
func WithAllowSchemeDowngrade ¶ added in v2.2.0
func WithAllowSchemeDowngrade(allow bool) RedirectOption
WithAllowSchemeDowngrade permits a redirect that downgrades the scheme (https on the original request -> http on the target). The default (false) refuses such a downgrade so a credential carried in a custom request header (which Go forwards across a redirect, stripping only Authorization/Cookie) is never sent over a cleartext hop. A scheme upgrade (http->https) is always allowed regardless of this setting. The downgrade is judged against the ORIGINAL request's scheme (via[0]).
func WithAllowedHosts ¶
func WithAllowedHosts(hosts ...string) RedirectOption
WithAllowedHosts sets the exact hostnames allowed as redirect targets.
func WithAllowedSuffixes ¶
func WithAllowedSuffixes(suffixes ...string) RedirectOption
WithAllowedSuffixes sets the domain suffixes allowed (e.g. ".docker.com").
func WithMaxHops ¶
func WithMaxHops(n int) RedirectOption
WithMaxHops sets the maximum number of redirect hops. Default: 5.
func WithSameHost ¶ added in v2.2.0
func WithSameHost() RedirectOption
WithSameHost additionally allows a redirect whose target host equals the original request's host (ASCII case-insensitive, RFC 3986 §6.2.2.1), in addition to any WithAllowedHosts / WithAllowedSuffixes entries. It is the building block for a same-origin policy: combined with the default scheme-downgrade refusal (see WithAllowSchemeDowngrade), it follows a service's own same-host redirects (including an http->https upgrade) while refusing a cross-host hop that would forward a custom auth header to another origin. A policy built with only WithSameHost (no allowlisted hosts) permits exactly the same-host set.
type ResponseTooLargeError ¶
type ResponseTooLargeError struct {
Limit int64
}
ResponseTooLargeError is returned by Retry when the response body exceeds the configured maximum (WithMaxBodyBytes, default DefaultMaxBodyBytes). The body is not returned: a truncated payload indistinguishable from a complete one is a silent-corruption hazard, so Retry fails loud instead. Limit is the cap that was exceeded, mirroring the stdlib *http.MaxBytesError shape.
func (*ResponseTooLargeError) Error ¶
func (e *ResponseTooLargeError) Error() string
type RetryAfterHint ¶ added in v2.2.0
RetryAfterHint is implemented by errors that carry an explicit wait duration for the next retry, typically a parsed and capped Retry-After. When fn's returned error is transient AND implements this interface with a positive duration, RetryWithBackoff waits that duration before the next attempt instead of its jittered exponential backoff. The exponential base still advances, so a later transient error without a hint resumes the normal progression. The hint MUST already be capped by the implementer (e.g. via ParseRetryAfter); RetryWithBackoff sleeps on it verbatim and applies no ceiling of its own, so an uncapped value is an unbounded-wait hazard.
type RetryRoundTripper ¶
type RetryRoundTripper struct {
// contains filtered or unexported fields
}
RetryRoundTripper implements http.RoundTripper with automatic retry.
By default, only idempotent methods (GET, HEAD, OPTIONS, TRACE) are retried. Use WithRetryNonIdempotent to also retry POST/PUT/PATCH/DELETE when the request has a GetBody function for body replay.
Inspired by hashicorp/go-retryablehttp, but operates directly on stdlib *http.Request without a custom request type, and counts TOTAL attempts (WithRTMaxAttempts) rather than go-retryablehttp's retries-beyond-first.
func NewRetryRoundTripper ¶
func NewRetryRoundTripper(next http.RoundTripper, opts ...RTOption) *RetryRoundTripper
NewRetryRoundTripper creates a RetryRoundTripper wrapping next with the given options. If next is nil, http.DefaultTransport is used.
func (*RetryRoundTripper) RoundTrip ¶
RoundTrip implements http.RoundTripper. It retries eligible requests on transient failures with jittered exponential backoff, honoring Retry-After. Per the http.RoundTripper contract, the caller's request is never mutated.
When retries are exhausted the final response is returned, not an error: if the last attempt produced a retryable response (e.g. a 503), RoundTrip returns that *http.Response with a nil error, exactly as a non-retried request would. The caller owns the returned body (must Close it) and MUST inspect resp.StatusCode - a nil error does not imply a 2xx response.
func (*RetryRoundTripper) StandardClient ¶
func (rt *RetryRoundTripper) StandardClient() *http.Client
StandardClient returns an *http.Client using this RetryRoundTripper as its Transport. Mirrors hashicorp/go-retryablehttp StandardClient().
The returned client sets no Client.Timeout. Bound every request with a context deadline (http.NewRequestWithContext) or configure the wrapped transport's own timeouts: without one, a stalled upstream blocks RoundTrip indefinitely, the retry loop never advances (it is suspended inside the transport), and because the RoundTripper logs nothing itself the stall is silent. A Client.Timeout is intentionally NOT set here because it would cap total time across all retries, conflicting with WithRTMaxElapsedTime.
The returned client sets no CheckRedirect: redirects follow the net/http default policy (up to 10 hops to any host; Authorization is stripped on cross-host redirects). This RoundTripper is a Transport and cannot restrict redirect targets from RoundTrip. Callers that need a redirect allowlist must set CheckRedirect on the returned client explicitly, e.g.:
c := rt.StandardClient()
c.CheckRedirect = httpx.DefaultRedirectPolicy // same-host only
// or httpx.RedirectPolicyFunc(httpx.WithAllowedSuffixes(".example.com"))
type StatusError ¶
StatusError represents a non-2xx response with URL context. Used by Retry. Supports errors.Is matching against ErrRateLimited and ErrServerError.
func (*StatusError) Error ¶
func (e *StatusError) Error() string
func (*StatusError) Is ¶
func (e *StatusError) Is(target error) bool
Is reports whether this StatusError matches ErrRateLimited or ErrServerError.
type Transient ¶
type Transient interface {
IsTransient() bool
}
Transient is the interface for errors that can report whether they represent a transient (retryable) failure.
type Validators ¶ added in v2.6.0
type Validators struct {
// ETag is replayed as If-None-Match.
ETag string
// LastModified is replayed as If-Modified-Since.
LastModified string
}
Validators carries the cache validators captured from a previous 200 response, replayed on the next conditional request so an unchanged resource is a cheap 304 instead of a re-download. Persist them alongside the cached body; the zero value sends no conditional headers (forcing a full 200).