httpclient

package
v2.1.0 Latest Latest
Warning

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

Go to latest
Published: May 2, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

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

View Source
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 Bulkhead

type Bulkhead interface {
	Acquire(ctx context.Context) (func(), error)
}

Bulkhead limits concurrency for outbound requests.

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.

func New

func New(opts Options) *Client

New constructs an outbound HTTP client with sane defaults.

func (*Client) Do

func (c *Client) Do(req *http.Request) (*http.Response, error)

Do issues the HTTP request with retry behavior when configured. Callers that need SSRF protection must supply an SSRFTransport and redirect policy via Options so target validation is enforced at the transport layer.

type FailureFunc

type FailureFunc func(*http.Response, error) bool

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 Resolver

type Resolver interface {
	LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error)
}

Resolver resolves hostnames for SSRF protection.

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

func (t *SSRFTransport) CheckRedirect(req *http.Request, via []*http.Request) error

CheckRedirect validates redirect targets and enforces scheme-change rules.

func (*SSRFTransport) RoundTrip

func (t *SSRFTransport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip validates the outbound request before dialing.

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.

func (*SemaphoreBulkhead) Acquire

func (b *SemaphoreBulkhead) Acquire(ctx context.Context) (func(), error)

Acquire reserves capacity until the release function is called.

Jump to

Keyboard shortcuts

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