-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathrequest.go
281 lines (248 loc) · 8.16 KB
/
request.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
package httpclient
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"github.com/cenkalti/backoff/v4"
)
// Request is a type used for configuring, performing and decoding HTTP
// requests
type Request struct {
err error
client *http.Client // DO NOT MODIFY THIS CLIENT
method string
baseURL string
path string
headers []header
expectedStatus int // The statusCode that is expected for a success
retryCount int // Number of times to retry
body io.ReadWriter
ctx context.Context
}
// WithBody sets the body on the request with the passed io.ReadWriter
func (r *Request) WithBody(body io.ReadWriter) *Request {
r.body = body
return r
}
// WithBytes sets the passed bytes as the body to be used on the Request
func (r *Request) WithBytes(body []byte) *Request {
return r.WithBody(bytes.NewBuffer(body))
}
// WithString sets the passed string as the body to be used on the Request
func (r *Request) WithString(body string) *Request {
return r.WithBody(bytes.NewBufferString(body))
}
// WithForm encodes and sets the passed url.Values as the body to be used on
// the Request
func (r *Request) WithForm(data url.Values) *Request {
return r.WithBody(bytes.NewBufferString(data.Encode())).
WithContentType("application/x-www-form-urlencoded")
}
// WithJSON sets the JSON encoded passed interface as the body to be used on
// the Request
func (r *Request) WithJSON(body interface{}) *Request {
r.body = bytes.NewBuffer(nil)
r.err = json.NewEncoder(r.body).Encode(body)
return r.WithContentType("application/json")
}
// WithXML sets the XML encoded passed interface as the body to be used on the
// Request
func (r *Request) WithXML(body interface{}) *Request {
r.body = bytes.NewBuffer(nil)
r.err = xml.NewEncoder(r.body).Encode(body)
return r.WithContentType("application/xml")
}
// WithContext sets the context on the Request
func (r *Request) WithContext(ctx context.Context) *Request {
r.ctx = ctx
return r
}
// WithContentType sets the content-type that will be set in the headers on the
// Request
func (r *Request) WithContentType(typ string) *Request {
return r.WithHeader("Content-Type", typ)
}
// WithHeader sets a header that will be used on the Request
func (r *Request) WithHeader(key, value string) *Request {
r.headers = append(r.headers, header{key: key, value: value})
return r
}
// WithExpectedStatus sets the desired status-code that will be a success. If
// the expected status code isn't received an error will be returned or the
// request will be retried if a count has been set with WithRetry(...)
func (r *Request) WithExpectedStatus(expectedStatusCode int) *Request {
r.expectedStatus = expectedStatusCode
return r
}
// WithRetry sets the desired number of retries on the Request
// Note: In order to trigger retries you must set an expected status code with
// the WithExpectedStatus(...) method
func (r *Request) WithRetry(retryCount int) *Request {
r.retryCount = retryCount
return r
}
// String is a convenience method that handles executing, defer closing, and
// decoding the body into a string before returning
func (r *Request) String() (string, error) {
bytes, err := r.Bytes()
if err != nil {
return "", err
}
return string(bytes), nil
}
// Bytes is a convenience method that handles executing, defer closing, and
// decoding the body into a slice of bytes before returning
func (r *Request) Bytes() ([]byte, error) {
res, err := r.Do()
if err != nil {
return nil, err
}
defer res.Close()
if r.expectedStatus > 0 && res.StatusCode() != r.expectedStatus {
return nil, fmt.Errorf("Unexpected status received : %s", res.Status())
}
return res.Bytes()
}
// JSON is a convenience method that handles executing, defer closing, and
// decoding the JSON body into the passed interface before returning
func (r *Request) JSON(out interface{}) error {
res, err := r.Do()
if err != nil {
return err
}
defer res.Close()
if r.expectedStatus > 0 && res.StatusCode() != r.expectedStatus {
return fmt.Errorf("Unexpected status received : %s", res.Status())
}
return res.JSON(out)
}
// JSONWithError is identical to the JSON(...) method but also takes an errOut
// interface for when the status code isn't expected. In this case the response
// body will be decoded into the errOut interface and the boolean (expected)
// will return false
func (r *Request) JSONWithError(out interface{}, errOut interface{}) (bool, error) {
res, err := r.Do()
if err != nil {
return false, err
}
defer res.Close()
if r.expectedStatus > 0 && res.StatusCode() != r.expectedStatus {
return false, res.JSON(errOut)
}
return true, res.JSON(out)
}
// XML is a convenience method that handles executing, defer closing, and
// decoding the XML body into the passed interface before returning
func (r *Request) XML(out interface{}) error {
res, err := r.Do()
if err != nil {
return err
}
defer res.Close()
if r.expectedStatus > 0 && res.StatusCode() != r.expectedStatus {
return fmt.Errorf("Unexpected status received : %s", res.Status())
}
return res.XML(out)
}
// XMLWithError is identical to the XML(...) method but also takes an errOut
// interface for when the status code isn't expected. In this case the response
// body will be decoded into the errOut interface and the boolean (expected)
// will return false
func (r *Request) XMLWithError(out interface{}, errOut interface{}) (bool, error) {
res, err := r.Do()
if err != nil {
return false, err
}
defer res.Close()
if r.expectedStatus > 0 && res.StatusCode() != r.expectedStatus {
return false, res.XML(errOut)
}
return true, res.XML(out)
}
// Error performs the request and returns any errors that result from the Do
func (r *Request) Error() error {
res, err := r.Do()
if err != nil {
return err
}
defer res.Close()
if r.expectedStatus > 0 && res.StatusCode() != r.expectedStatus {
return fmt.Errorf("Unexpected status received : %s", res.Status())
}
return nil
}
// Do performs the base request and returns a populated Response
// NOTE: As with the standard library, when calling Do you must remember to
// close the response body : res.Body.Close()
func (r *Request) Do() (*Response, error) {
if r.err != nil {
return nil, r.err
}
// Convert the Request to a standard http.Request
req, err := r.toHTTPRequest()
if err != nil {
return nil, err
}
// Perform the request with retries, returning the wrapped http.Response
res, err := doRetry(r.client, req, r.expectedStatus, r.retryCount)
if err != nil {
return nil, err
}
return &Response{res: res}, nil
}
// toHTTPRequest converts a Request to a standard HTTP Request. It assumes
// there is no error on the request.
func (r *Request) toHTTPRequest() (*http.Request, error) {
// Generate a new http Request using client and passed Request
req, err := http.NewRequest(r.method, r.baseURL+r.path, r.body)
if err != nil {
return nil, err
}
// Apply a context if one is set on the Request
if r.ctx != nil {
req = req.WithContext(r.ctx)
}
// Apply all headers from both the client and the Request
for _, h := range r.headers {
req.Header.Set(h.key, h.value)
}
return req, nil
}
// doRetry executes the passed http Request using the passed http Client and
// retries as many times as specified
func doRetry(c *http.Client, r *http.Request, expectedStatus, retryCount int) (*http.Response, error) {
// Create a ticker that will execute the exponential backoff algorithm
ticker := backoff.NewTicker(backoff.NewExponentialBackOff())
// Define the return variables
var res *http.Response
var err error
// Continuously retry HTTP requests
tries := 0
for range ticker.C {
tries++ // Increment the tries value to indicate which try num we're on
// Perform the request using the standard library
res, err = c.Do(r)
if err != nil {
return nil, err
}
// If the status code isn't what we expect
if expectedStatus > 0 && expectedStatus != res.StatusCode {
if retryCount > tries {
continue // Retry if we should
}
err = fmt.Errorf("request failed to get expected status after %v retries", retryCount)
}
// Stop the ticker and break out of the tick loop
ticker.Stop()
break
}
if err != nil {
return nil, err
}
return res, nil
}