Files
2026-02-22 15:21:18 -05:00

74 lines
1.6 KiB
Go

package httputil
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
type RetryConfig struct {
MaxAttempts int
BaseDelay time.Duration
MaxDelay time.Duration
}
var DefaultRetry = RetryConfig{
MaxAttempts: 3,
BaseDelay: 1 * time.Second,
MaxDelay: 10 * time.Second,
}
// Do executes an HTTP request with exponential backoff retry.
// The buildReq function is called on each attempt to produce a fresh request
// (required because request bodies are consumed on each attempt).
func Do(ctx context.Context, client *http.Client, cfg RetryConfig, buildReq func() (*http.Request, error)) (*http.Response, error) {
if cfg.MaxAttempts <= 0 {
cfg.MaxAttempts = DefaultRetry.MaxAttempts
}
var lastErr error
delay := cfg.BaseDelay
for attempt := 1; attempt <= cfg.MaxAttempts; attempt++ {
req, err := buildReq()
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
resp, err := client.Do(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if err != nil {
lastErr = err
} else {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
resp.Body.Close()
lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
if attempt == cfg.MaxAttempts {
break
}
fmt.Printf("[RETRY] Attempt %d/%d failed: %v — retrying in %s\n",
attempt, cfg.MaxAttempts, lastErr, delay)
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
delay *= 2
if delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}
}
return nil, fmt.Errorf("all %d attempts failed, last error: %w", cfg.MaxAttempts, lastErr)
}