-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathhttp_client.go
More file actions
223 lines (189 loc) Β· 5.79 KB
/
Copy pathhttp_client.go
File metadata and controls
223 lines (189 loc) Β· 5.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package whatsonchain
import (
"bytes"
"crypto/rand"
"io"
"math"
"math/big"
"net/http"
"time"
)
// ExponentialBackoff provides exponential backoff functionality
type ExponentialBackoff struct {
initialTimeout time.Duration
maxTimeout time.Duration
exponentFactor float64
maxJitterInterval time.Duration
}
// NewExponentialBackoff creates a new exponential backoff instance
func NewExponentialBackoff(initialTimeout, maxTimeout time.Duration, exponentFactor float64, maxJitterInterval time.Duration) *ExponentialBackoff {
return &ExponentialBackoff{
initialTimeout: initialTimeout,
maxTimeout: maxTimeout,
exponentFactor: exponentFactor,
maxJitterInterval: maxJitterInterval,
}
}
// NextInterval calculates the next backoff interval for the given attempt
func (eb *ExponentialBackoff) NextInterval(attempt int) time.Duration {
if attempt < 0 {
attempt = 0
}
// Calculate exponential backoff: initialTimeout * (exponentFactor ^ attempt)
backoffDuration := float64(eb.initialTimeout) * math.Pow(eb.exponentFactor, float64(attempt))
// Cap at maxTimeout
if backoffDuration > float64(eb.maxTimeout) {
backoffDuration = float64(eb.maxTimeout)
}
// Add jitter to prevent thundering herd
var jitter time.Duration
if eb.maxJitterInterval > 0 {
// Use crypto/rand for better security
if maxJitter := big.NewInt(int64(eb.maxJitterInterval)); maxJitter.Int64() > 0 {
if n, err := rand.Int(rand.Reader, maxJitter); err == nil {
jitter = time.Duration(n.Int64())
}
}
}
return time.Duration(backoffDuration) + jitter
}
// RetryableHTTPClient is a native Go HTTP client with retry capability
type RetryableHTTPClient struct {
client *http.Client
retryCount int
backoff *ExponentialBackoff
}
// NewRetryableHTTPClient creates a new retryable HTTP client
func NewRetryableHTTPClient(httpClient *http.Client, retryCount int, backoff *ExponentialBackoff) *RetryableHTTPClient {
if httpClient == nil {
httpClient = &http.Client{}
}
return &RetryableHTTPClient{
client: httpClient,
retryCount: retryCount,
backoff: backoff,
}
}
// Do will execute an HTTP request with retry logic
func (r *RetryableHTTPClient) Do(req *http.Request) (*http.Response, error) {
var lastResp *http.Response
var lastErr error
// If no retries configured, just execute once
if r.retryCount <= 0 {
return r.client.Do(req)
}
// Read and store the request body once so we can reuse it for retries
var bodyBytes []byte
if req.Body != nil {
var err error
bodyBytes, err = io.ReadAll(req.Body)
if err != nil {
return nil, err
}
_ = req.Body.Close()
}
maxAttempts := r.retryCount + 1 // retryCount doesn't include the initial attempt
for attempt := 0; attempt < maxAttempts; attempt++ {
// Create a new request for each attempt
var reqForAttempt *http.Request
var err error
if bodyBytes != nil {
// Create new request with fresh body
reqForAttempt, err = http.NewRequestWithContext( //nolint:gosec // G704: URL is controlled by this library, not user input
req.Context(),
req.Method,
req.URL.String(),
bytes.NewReader(bodyBytes),
)
} else {
// There is no "body", just clone the request
reqForAttempt, err = http.NewRequestWithContext( //nolint:gosec // G704: URL is controlled by this library, not user input
req.Context(),
req.Method,
req.URL.String(),
nil,
)
}
if err != nil {
return nil, err
}
// Copy headers from original request
for key, values := range req.Header {
for _, value := range values {
reqForAttempt.Header.Add(key, value)
}
}
// Execute the request
var resp *http.Response
resp, err = r.client.Do(reqForAttempt) //nolint:gosec // G704: URL is controlled by this library, not user input
// If this is the last attempt, return whatever we got
if attempt == maxAttempts-1 {
return resp, err
}
// Check if we should retry
if !r.shouldRetry(resp, err) {
return resp, err
}
// Store the response/error for potential return
lastResp = resp
lastErr = err
// Close the response body if it exists (to free up the connection)
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
// Calculate backoff duration
var backoffDuration time.Duration
if r.backoff != nil {
backoffDuration = r.backoff.NextInterval(attempt)
} else {
// Default exponential backoff if none provided
backoffDuration = time.Duration(math.Pow(2, float64(attempt))) * time.Millisecond * 100
}
// Wait before retrying, respecting context cancellation
timer := time.NewTimer(backoffDuration)
select {
case <-req.Context().Done():
timer.Stop()
return lastResp, req.Context().Err()
case <-timer.C:
// Continue to next attempt
}
}
return lastResp, lastErr
}
// shouldRetry determines if a request should be retried based on the response
func (r *RetryableHTTPClient) shouldRetry(resp *http.Response, err error) bool {
// Retry on network errors
if err != nil {
return true
}
// Retry on server errors (5xx) and specific client errors
if resp != nil {
switch resp.StatusCode {
case http.StatusInternalServerError, // 500
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout, // 504
http.StatusTooManyRequests: // 429
return true
}
}
return false
}
// SimpleHTTPClient is a simple wrapper around http.Client for non-retry scenarios
type SimpleHTTPClient struct {
client *http.Client
}
// NewSimpleHTTPClient creates a new simple HTTP client wrapper
func NewSimpleHTTPClient(httpClient *http.Client) *SimpleHTTPClient {
if httpClient == nil {
httpClient = &http.Client{}
}
return &SimpleHTTPClient{
client: httpClient,
}
}
// Do executes an HTTP request without retry logic
func (s *SimpleHTTPClient) Do(req *http.Request) (*http.Response, error) {
return s.client.Do(req)
}