Documentation
¶
Overview ¶
Package httpclient provides outbound HTTP clients with retry and tracing.
Package httpclient provides outbound HTTP client adapters with retry-aware defaults.
Retry defaults -------------
By default, retry is enabled only for safe read-only methods:
- GET - HEAD
Caller code should add additional methods only when explicit idempotency guarantees exist for the upstream contract.
Use RetryableMethods to opt in, for example to allow PUT retries when you know every retried payload is safe to replay:
client := httpclient.New(httpclient.Options{
Retry: httpclient.RetryOptions{
MaxRetries: 2,
RetryableMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut},
},
})
Retry migration path -------------------
If you previously retried non-idempotent methods broadly, migrate in phases:
1) Roll out with defaults (GET/HEAD only) and confirm request-level SLIs. 2) Add method opt-in only where upstream call semantics are explicitly replay-safe.
Example:
client := httpclient.New(httpclient.Options{
Retry: httpclient.RetryOptions{
MaxRetries: 2,
RetryableMethods: []string{
http.MethodGet,
http.MethodHead,
// Enable PUT only after upstream replay semantics are reviewed.
http.MethodPut,
},
UseRetryAfter: true,
},
})
Retry budgets -------------
Configure max budget controls for backoff and Retry-After parsing:
client := httpclient.New(httpclient.Options{
Retry: httpclient.RetryOptions{
MaxRetries: 5,
MaxElapsedTime: 2 * time.Second,
MinBackoff: 100 * time.Millisecond,
MaxBackoff: 2 * time.Second,
UseRetryAfter: true,
},
})
A retry loop stops when elapsed delay would exceed MaxElapsedTime.
Example (RetryDefaultsFavorIdempotentMethods) ¶
var calls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&calls, 1)
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer server.Close()
client := New(Options{
Sleep: func(time.Duration) {},
Retry: RetryOptions{
MaxRetries: 1,
},
})
getReq, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
getResp, _ := client.Do(getReq)
if getResp != nil && getResp.Body != nil {
_ = getResp.Body.Close()
}
postReq, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, server.URL, nil)
postResp, _ := client.Do(postReq)
if postResp != nil && postResp.Body != nil {
_ = postResp.Body.Close()
}
fmt.Printf("attempts=%d\n", atomic.LoadInt32(&calls))
Output: attempts=3
Example (RetryUnsafeMethodsMustBeOptedIn) ¶
var calls int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&calls, 1)
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer server.Close()
client := New(Options{
Sleep: func(time.Duration) {},
Retry: RetryOptions{
MaxRetries: 1,
RetryableMethods: []string{http.MethodPut},
},
})
req, _ := http.NewRequestWithContext(context.Background(), http.MethodPut, server.URL, nil)
req.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
}
resp, _ := client.Do(req)
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
fmt.Printf("attempts=%d\n", atomic.LoadInt32(&calls))
Output: attempts=2
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrBreakerOpen = errors.New("circuit breaker open")
ErrBreakerOpen is returned when the circuit is open.
Functions ¶
This section is empty.
Types ¶
type Breaker ¶
type Breaker interface {
Execute(fn func() (*http.Response, error), isFailure FailureFunc) (*http.Response, error)
}
Breaker executes a function with circuit breaker semantics.
type CircuitBreaker ¶
type CircuitBreaker struct {
// contains filtered or unexported fields
}
CircuitBreaker provides a simple failure-count breaker.
func NewCircuitBreaker ¶
func NewCircuitBreaker(opts CircuitBreakerOptions) *CircuitBreaker
NewCircuitBreaker creates a circuit breaker with sane defaults.
func (*CircuitBreaker) Execute ¶
func (b *CircuitBreaker) Execute(fn func() (*http.Response, error), isFailure FailureFunc) (*http.Response, error)
Execute runs fn when the circuit allows it, updating breaker state afterward.
type CircuitBreakerOptions ¶
type CircuitBreakerOptions struct {
FailureThreshold int
SuccessThreshold int
OpenTimeout time.Duration
HalfOpenMaxInFlight int
Now func() time.Time
}
CircuitBreakerOptions configures a circuit breaker.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client implements ports.HTTPClient.
type FailureFunc ¶
FailureFunc determines whether a response or error should count as a failure.
type Options ¶
type Options struct {
Timeout time.Duration
Transport http.RoundTripper
CheckRedirect func(*http.Request, []*http.Request) error
DisableTracing bool
Sleep func(time.Duration)
Retry RetryOptions
Breaker Breaker
BreakerFailure FailureFunc
Bulkhead Bulkhead
}
Options configures the outbound HTTP client.
type RetryOptions ¶
type RetryOptions struct {
Disable bool
MaxRetries int
MaxElapsedTime time.Duration
MinBackoff time.Duration
MaxBackoff time.Duration
RetryableStatusCodes []int
RetryableMethods []string
UseRetryAfter bool
RetryOn func(*http.Response, error) bool
}
RetryOptions configures retry behavior.
type SSRFOptions ¶
type SSRFOptions struct {
Transport *http.Transport
Resolver Resolver
Dialer *net.Dialer
AllowedHosts []string
AllowedPorts []int
AllowedCIDRs []string
// AllowRedirectSchemeChange permits http<->https redirects.
AllowRedirectSchemeChange bool
}
SSRFOptions configures SSRF protection for outbound HTTP.
type SSRFTransport ¶
type SSRFTransport struct {
// contains filtered or unexported fields
}
SSRFTransport validates outbound targets before dialing and on redirects.
Threat model coverage: - Blocks private/reserved IPv4/IPv6 ranges (including link-local/metadata IPs). - Re-validates each redirect hop (host/port/scheme) via CheckRedirect. - Mitigates DNS rebinding by resolving once and dialing by IP.
Non-goals: - If you allowlist a host/CIDR, requests to that target are allowed. - It does not inspect application-level SSRF beyond host/port/scheme/IP.
Example:
guard, _ := httpclient.NewSSRFTransport(httpclient.SSRFOptions{
AllowedHosts: []string{"api.example.com"},
AllowedPorts: []int{443},
})
client := &http.Client{
Transport: guard,
CheckRedirect: guard.CheckRedirect,
}
func NewSSRFTransport ¶
func NewSSRFTransport(opts SSRFOptions) (*SSRFTransport, error)
NewSSRFTransport creates a transport that blocks private/reserved networks by default.
func (*SSRFTransport) CheckRedirect ¶
CheckRedirect validates redirect targets and enforces scheme-change rules.
type SemaphoreBulkhead ¶
type SemaphoreBulkhead struct {
// contains filtered or unexported fields
}
SemaphoreBulkhead bounds concurrent requests using a semaphore.
func NewSemaphoreBulkhead ¶
func NewSemaphoreBulkhead(limit int) (*SemaphoreBulkhead, error)
NewSemaphoreBulkhead creates a semaphore bulkhead with the given limit.