11package http
22
33import (
4+ "log/slog"
5+ "math"
46 "net/http"
57 info "offi/internal/build_info"
68 "offi/internal/tracing"
9+ "strconv"
10+ "time"
711
812 "github.com/go-chi/transport"
913)
@@ -17,8 +21,87 @@ func Transport(withRetries bool) http.RoundTripper {
1721 }
1822
1923 if withRetries {
20- base = append (base , transport . Retry (transport .Chain (http .DefaultTransport , uaTransport ), 3 ))
24+ base = append (base , Retry (transport .Chain (http .DefaultTransport , uaTransport ), 3 ))
2125 }
2226
2327 return transport .Chain (http .DefaultTransport , base ... )
2428}
29+
30+ func Retry (baseTransport http.RoundTripper , maxRetries int ) func (http.RoundTripper ) http.RoundTripper {
31+ return func (next http.RoundTripper ) http.RoundTripper {
32+ return transport .RoundTripFunc (func (req * http.Request ) (resp * http.Response , err error ) {
33+ defer func () {
34+ if isRetryable (resp ) {
35+ for i := 1 ; i <= maxRetries ; i ++ {
36+ wait := backOff (resp , i )
37+
38+ timer := time .NewTimer (wait )
39+
40+ slog .DebugContext (req .Context (), "waiting before retrying request" , "wait_time" , wait .String ())
41+
42+ select {
43+ case <- req .Context ().Done ():
44+ timer .Stop ()
45+ err = req .Context ().Err ()
46+ break
47+ case <- timer .C :
48+ }
49+
50+ resp , err = baseTransport .RoundTrip (req )
51+ if ! isRetryable (resp ) {
52+ break
53+ }
54+
55+ slog .InfoContext (req .Context (), "retrying request" ,
56+ "target_host" , req .URL .Host ,
57+ "attempt" , i ,
58+ )
59+ }
60+ }
61+ }()
62+
63+ return next .RoundTrip (req )
64+ })
65+ }
66+ }
67+
68+ func backOff (resp * http.Response , attempt int ) time.Duration {
69+ minDuration := 1 * time .Second
70+ maxDuration := 16 * time .Second
71+
72+ if resp .StatusCode == http .StatusTooManyRequests || resp .StatusCode == http .StatusServiceUnavailable {
73+ if s , ok := resp .Header ["Retry-After" ]; ok {
74+ if sleep , err := strconv .ParseInt (s [0 ], 10 , 64 ); err == nil {
75+ return time .Second * time .Duration (sleep )
76+ }
77+ }
78+ }
79+
80+ // simple exp. backoff
81+ mult := math .Pow (2 , float64 (attempt )) * float64 (minDuration )
82+ sleep := time .Duration (mult )
83+ if float64 (sleep ) != mult || sleep > maxDuration {
84+ sleep = maxDuration
85+ }
86+ return sleep
87+ }
88+
89+ func isRetryable (resp * http.Response ) bool {
90+ if resp == nil {
91+ return false
92+ }
93+
94+ // 429 Too Many Requests is recoverable. Sometimes the server puts
95+ // Retry-After response header to indicate when the server is will be available again
96+ if resp .StatusCode == http .StatusTooManyRequests {
97+ return true
98+ }
99+
100+ // We retry on 500-range responses to allow the server time to recover, as 500's are typically not permanent
101+ // errors and may relate to outages on the server side.
102+ if resp .StatusCode == 0 || (resp .StatusCode > 500 && resp .StatusCode != http .StatusNotImplemented ) {
103+ return true
104+ }
105+
106+ return false
107+ }
0 commit comments