1package typesafe23import (4 "context"5 "errors"6 "fmt"7 "math"8 "math/rand/v2"9 "net/http"10 "strconv"11 "time"12)1314// RetryPolicy controls retries after transient failures. All delays must be15// nonnegative, MaxDelay must be at least InitialDelay, and Jitter is in [0, 1].16// Retryable HTTP statuses are 408, 429, and 500–599. A caller's cancellation or17// deadline always stops the operation, regardless of this policy.18type RetryPolicy struct {19 MaxRetries int20 InitialDelay time.Duration21 MaxDelay time.Duration22 MaxRetryAfter time.Duration23 Jitter float6424 RetryConnectionErrors bool25 RetryTimeouts bool26}2728// DefaultRetryPolicy returns an independent copy of the default settings:29// two retries, 500ms initial backoff, 5s maximum backoff, up to 25% downward30// jitter, and server-requested delays up to 60s. Network failures and attempt31// timeouts are retried. No total time budget is imposed; use a context deadline.32func DefaultRetryPolicy() RetryPolicy {33 return RetryPolicy{34 MaxRetries: 2, InitialDelay: 500 * time.Millisecond,35 MaxDelay: 5 * time.Second, MaxRetryAfter: 60 * time.Second,36 Jitter: 0.25, RetryConnectionErrors: true, RetryTimeouts: true,37 }38}3940func (p RetryPolicy) validate() error {41 if p.MaxRetries < 0 || p.InitialDelay < 0 || p.MaxDelay < p.InitialDelay || p.MaxRetryAfter < 0 {42 return fmt.Errorf("typesafe: invalid retry count or delays")43 }44 if math.IsNaN(p.Jitter) || p.Jitter < 0 || p.Jitter > 1 {45 return fmt.Errorf("typesafe: retry jitter must be between 0 and 1")46 }47 return nil48}4950func retryableStatus(status int) bool {51 return status == 408 || status == 429 || (status >= 500 && status <= 599)52}5354func (p RetryPolicy) retriesError(err error) bool {55 if _, ok := errors.AsType[*ResponseError](err); ok {56 return false57 }58 if transport, ok := errors.AsType[*transportError](err); ok {59 if transport.timeout {60 return p.RetryTimeouts61 }62 return p.RetryConnectionErrors63 }64 return false65}6667func retryDelay(policy RetryPolicy, attempt int, headers http.Header) time.Duration {68 if delay, ok := serverDelay(headers, policy.MaxRetryAfter); ok {69 return delay70 }71 delay := policy.InitialDelay72 for range attempt {73 if delay == 0 {74 break75 }76 if delay >= policy.MaxDelay-delay {77 delay = policy.MaxDelay78 break79 }80 delay *= 281 }82 return delay - time.Duration(float64(delay)*rand.Float64()*policy.Jitter)83}8485func serverDelay(headers http.Header, maximum time.Duration) (time.Duration, bool) {86 if raw := headers.Get("retry-after-ms"); raw != "" {87 if delay, ok := numericDelay(raw, time.Millisecond, maximum); ok {88 return delay, true89 }90 }91 raw := headers.Get("Retry-After")92 if delay, ok := numericDelay(raw, time.Second, maximum); ok {93 return delay, true94 }95 date, err := http.ParseTime(raw)96 if err != nil {97 return 0, false98 }99 delay := max(time.Until(date), 0)100 return delay, delay <= maximum101}102103func numericDelay(raw string, unit, maximum time.Duration) (time.Duration, bool) {104 number, err := strconv.ParseFloat(raw, 64)105 if err != nil || math.IsNaN(number) || math.IsInf(number, 0) || number < 0 {106 return 0, false107 }108 nanoseconds := number * float64(unit)109 if nanoseconds >= float64(math.MaxInt64) || nanoseconds > float64(maximum) {110 return 0, false111 }112 return time.Duration(nanoseconds), true113}114115func waitForRetry(ctx context.Context, delay time.Duration) error {116 timer := time.NewTimer(delay)117 defer timer.Stop()118 select {119 case <-ctx.Done():120 return ctx.Err()121 case <-timer.C:122 return ctx.Err()123 }124}