53 lines
httpclient/retry.go
Retry client that re-attempts failed HTTP requests with linear backoff.
// Package httpclient provides an HTTP client with automatic retry for transient failures.
package httpclient
 
import (
	"net/http"
	"time"
)
 
// RetryClient wraps an http.Client and retries on server errors and network failures.
type RetryClient struct {
	client     *http.Client
	maxRetries int
	backoff    time.Duration
}
 
// NewRetryClient returns a RetryClient that retries up to maxRetries additional times
// after the first attempt, waiting backoff between each pair of attempts.
func NewRetryClient(client *http.Client, maxRetries int, backoff time.Duration) *RetryClient {
	return &RetryClient{client: client, maxRetries: maxRetries, backoff: backoff}
}
 
// Do executes req and retries on 5xx responses and network errors.
// 4xx responses indicate a client error and are returned immediately without retrying.
// The request body, if present, is automatically rewound before each retry attempt.
func (c *RetryClient) Do(req *http.Request) (*http.Response, error) {
	var (
		resp *http.Response
		err  error
	)
	for attempt := 0; attempt <= c.maxRetries; attempt++ {
		if attempt > 0 {
			time.Sleep(c.backoff)
		}
		resp, err = c.client.Do(req)
		if err == nil && !shouldRetry(resp) {
			return resp, nil
		}
		if resp != nil {
			resp.Body.Close()
		}
	}
	return resp, err
}
 
// shouldRetry returns true when a response warrants another attempt.
// Network errors (nil resp) always warrant a retry.
// 4xx client errors must not be retried; only 5xx server errors should be.
func shouldRetry(resp *http.Response) bool {
	if resp == nil {
		return true
	}
	return resp.StatusCode >= 400
}