-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathhttpbackoff.go
288 lines (260 loc) · 11.4 KB
/
httpbackoff.go
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
// This package provides exponential backoff support for making HTTP requests.
//
// It uses the github.com/cenkalti/backoff algorithm.
//
// Network failures and HTTP 5xx status codes qualify for retries.
//
// HTTP calls that return HTTP 4xx status codes do not get retried.
//
// If the last HTTP request made does not result in a 2xx HTTP status code, an
// error is returned, together with the data.
//
// There are several utility methods that wrap the standard net/http package
// calls.
//
// Any function that takes no arguments and returns (*http.Response, error) can
// be retried using this library's Retry function.
//
// The methods in this library should be able to run concurrently in multiple
// go routines.
//
// # Example Usage
//
// Consider this trivial HTTP GET request:
//
// res, err := http.Get("http://www.google.com/robots.txt")
//
// This can be rewritten as follows, enabling automatic retries:
//
// res, attempts, err := httpbackoff.Get("http://www.google.com/robots.txt")
//
// The variable attempts stores the number of http calls that were made (one
// plus the number of retries).
package httpbackoff
import (
"bufio"
"bytes"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"time"
"github.com/cenkalti/backoff/v3"
)
var defaultClient Client = Client{
BackOffSettings: backoff.NewExponentialBackOff(),
}
type Client struct {
BackOffSettings *backoff.ExponentialBackOff
}
// Any non 2xx HTTP status code is considered a bad response code, and will
// result in a BadHttpResponseCode.
type BadHttpResponseCode struct {
HttpResponseCode int
Message string
}
// Returns an error message for this bad HTTP response code
func (err BadHttpResponseCode) Error() string {
return err.Message
}
// Retry is the core library method for retrying http calls.
//
// httpCall should be a function that performs the http operation, and returns
// (resp *http.Response, tempError error, permError error). Errors that should
// cause retries should be returned as tempError. Permanent errors that should
// not result in retries should be returned as permError. Retries are performed
// using the exponential backoff algorithm from the github.com/cenkalti/backoff
// package. Retry automatically treats HTTP 5xx status codes as a temporary
// error, and any other non-2xx HTTP status codes as a permanent error. Thus
// httpCall function does not need to handle the HTTP status code of resp,
// since Retry will take care of it.
//
// Concurrent use of this library method is supported.
func (httpRetryClient *Client) Retry(httpCall func() (resp *http.Response, tempError error, permError error)) (*http.Response, int, error) {
var tempError, permError error
var response *http.Response
attempts := 0
doHttpCall := func() error {
response, tempError, permError = httpCall()
attempts += 1
if tempError != nil {
return tempError
}
if permError != nil {
return nil
}
// only call this if there is a non 2xx response
body := func(response *http.Response) string {
// this is a no-op
raw, err := httputil.DumpResponse(response, true)
if err == nil {
return string(raw)
}
return ""
}
// now check if http response code is such that we should retry [500, 600) or 429...
if respCode := response.StatusCode; respCode/100 == 5 || respCode == 429 {
tempError = BadHttpResponseCode{
HttpResponseCode: respCode,
Message: "(Intermittent) HTTP response code " + strconv.Itoa(respCode) + "\n" + body(response),
}
return tempError
}
// now check http response code is ok [200, 300)...
if respCode := response.StatusCode; respCode/100 != 2 {
permError = BadHttpResponseCode{
HttpResponseCode: respCode,
Message: "(Permanent) HTTP response code " + strconv.Itoa(respCode) + "\n" + body(response),
}
return nil
}
return nil
}
// Make HTTP API calls using an exponential backoff algorithm...
b := backoff.ExponentialBackOff(*httpRetryClient.BackOffSettings)
_ = backoff.RetryNotify(doHttpCall, &b, func(err error, wait time.Duration) {
log.Printf("Error: %s", err)
})
switch {
case permError != nil:
return response, attempts, permError
case tempError != nil:
return response, attempts, tempError
default:
return response, attempts, nil
}
}
// Retry works the same as HTTPRetryClient.Retry, but uses the default exponential back off settings
func Retry(httpCall func() (resp *http.Response, tempError error, permError error)) (*http.Response, int, error) {
return defaultClient.Retry(httpCall)
}
// Retry wrapper for http://golang.org/pkg/net/http/#Get where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) Get(url string) (resp *http.Response, attempts int, err error) {
return httpRetryClient.Retry(func() (*http.Response, error, error) {
resp, err := http.Get(url)
// assume all errors should result in a retry
return resp, err, nil
})
}
// Get works the same as HTTPRetryClient.Get, but uses the default exponential back off settings
func Get(url string) (resp *http.Response, attempts int, err error) {
return defaultClient.Get(url)
}
// Retry wrapper for http://golang.org/pkg/net/http/#Head where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) Head(url string) (resp *http.Response, attempts int, err error) {
return httpRetryClient.Retry(func() (*http.Response, error, error) {
resp, err := http.Head(url)
// assume all errors should result in a retry
return resp, err, nil
})
}
// Head works the same as HTTPRetryClient.Head, but uses the default exponential back off settings
func Head(url string) (resp *http.Response, attempts int, err error) {
return defaultClient.Head(url)
}
// Retry wrapper for http://golang.org/pkg/net/http/#Post where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) Post(url string, bodyType string, body []byte) (resp *http.Response, attempts int, err error) {
return httpRetryClient.Retry(func() (*http.Response, error, error) {
resp, err := http.Post(url, bodyType, bytes.NewBuffer(body))
// assume all errors should result in a retry
return resp, err, nil
})
}
// Post works the same as HTTPRetryClient.Post, but uses the default exponential back off settings
func Post(url string, bodyType string, body []byte) (resp *http.Response, attempts int, err error) {
return defaultClient.Post(url, bodyType, body)
}
// Retry wrapper for http://golang.org/pkg/net/http/#PostForm where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) PostForm(url string, data url.Values) (resp *http.Response, attempts int, err error) {
return httpRetryClient.Retry(func() (*http.Response, error, error) {
resp, err := http.PostForm(url, data)
// assume all errors should result in a retry
return resp, err, nil
})
}
// PostForm works the same as HTTPRetryClient.PostForm, but uses the default exponential back off settings
func PostForm(url string, data url.Values) (resp *http.Response, attempts int, err error) {
return defaultClient.PostForm(url, data)
}
// Retry wrapper for http://golang.org/pkg/net/http/#Client.Do where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) ClientDo(c *http.Client, req *http.Request) (resp *http.Response, attempts int, err error) {
rawReq, err := httputil.DumpRequestOut(req, true)
// fatal
if err != nil {
return nil, 0, err
}
return httpRetryClient.Retry(func() (*http.Response, error, error) {
newReq, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(rawReq)))
newReq.RequestURI = ""
newReq.URL = req.URL
// If the original request doesn't explicitly set Accept-Encoding, then
// the go standard library will add it, and allow gzip compression, and
// magically unzip the response transparently. This wouldn't be too
// much of a problem, except that if the header is explicitly set, then
// the standard library won't automatically unzip the response. This is
// arguably a bug in the standard library but we'll work around it by
// checking this specific condition.
if req.Header.Get("Accept-Encoding") == "" {
newReq.Header.Del("Accept-Encoding")
}
if err != nil {
return nil, nil, err // fatal
}
resp, err := c.Do(newReq)
// assume all errors should result in a retry
return resp, err, nil
})
}
// ClientDo works the same as HTTPRetryClient.ClientDo, but uses the default exponential back off settings
func ClientDo(c *http.Client, req *http.Request) (resp *http.Response, attempts int, err error) {
return defaultClient.ClientDo(c, req)
}
// Retry wrapper for http://golang.org/pkg/net/http/#Client.Get where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) ClientGet(c *http.Client, url string) (resp *http.Response, attempts int, err error) {
return httpRetryClient.Retry(func() (*http.Response, error, error) {
resp, err := c.Get(url)
// assume all errors should result in a retry
return resp, err, nil
})
}
// ClientGet works the same as HTTPRetryClient.ClientGet, but uses the default exponential back off settings
func ClientGet(c *http.Client, url string) (resp *http.Response, attempts int, err error) {
return defaultClient.ClientGet(c, url)
}
// Retry wrapper for http://golang.org/pkg/net/http/#Client.Head where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) ClientHead(c *http.Client, url string) (resp *http.Response, attempts int, err error) {
return httpRetryClient.Retry(func() (*http.Response, error, error) {
resp, err := c.Head(url)
// assume all errors should result in a retry
return resp, err, nil
})
}
// ClientHead works the same as HTTPRetryClient.ClientHead, but uses the default exponential back off settings
func ClientHead(c *http.Client, url string) (resp *http.Response, attempts int, err error) {
return defaultClient.ClientHead(c, url)
}
// Retry wrapper for http://golang.org/pkg/net/http/#Client.Post where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) ClientPost(c *http.Client, url string, bodyType string, body []byte) (resp *http.Response, attempts int, err error) {
return httpRetryClient.Retry(func() (*http.Response, error, error) {
resp, err := c.Post(url, bodyType, bytes.NewBuffer(body))
// assume all errors should result in a retry
return resp, err, nil
})
}
// ClientPost works the same as HTTPRetryClient.ClientPost, but uses the default exponential back off settings
func ClientPost(c *http.Client, url string, bodyType string, body []byte) (resp *http.Response, attempts int, err error) {
return defaultClient.ClientPost(c, url, bodyType, body)
}
// Retry wrapper for http://golang.org/pkg/net/http/#Client.PostForm where attempts is the number of http calls made (one plus number of retries).
func (httpRetryClient *Client) ClientPostForm(c *http.Client, url string, data url.Values) (resp *http.Response, attempts int, err error) {
return httpRetryClient.Retry(func() (*http.Response, error, error) {
resp, err := c.PostForm(url, data)
// assume all errors should result in a retry
return resp, err, nil
})
}
// ClientPostForm works the same as HTTPRetryClient.ClientPostForm, but uses the default exponential back off settings
func ClientPostForm(c *http.Client, url string, data url.Values) (resp *http.Response, attempts int, err error) {
return defaultClient.ClientPostForm(c, url, data)
}