-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathhttpclient.go
355 lines (317 loc) · 10.6 KB
/
httpclient.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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
package httpclient
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/url"
"time"
resty "github.com/go-resty/resty/v2"
"github.com/slok/goresilience/circuitbreaker"
goresilienceErrors "github.com/slok/goresilience/errors"
"github.com/slok/goresilience/retry"
"golang.org/x/oauth2"
cc "golang.org/x/oauth2/clientcredentials"
)
var ErrCircuitOpen = goresilienceErrors.ErrCircuitOpen
type (
Callback func(func() (*Response, error)) (*Response, error)
Opt func(*HTTPClient)
HTTPClient struct {
resty *resty.Client
hostURL *url.URL
metrics Metrics
callbackChain Callback
}
)
// NewHTTPClient instantiates a new HTTPClient.
//
// Parameters:
//
// logger: interface is used to log request and response details.
// options: specifies options to HTTPClient.
func NewHTTPClient(logger resty.Logger, options ...Opt) *HTTPClient {
return newClient(resty.New().SetLogger(logger).GetClient(), options...)
}
func newClient(customClient *http.Client, options ...Opt) *HTTPClient {
client := &HTTPClient{
resty: resty.NewWithClient(customClient),
callbackChain: noopCallback,
}
for _, option := range options {
option(client)
}
return client
}
// GetClient returns the current http.Client.
func (c *HTTPClient) GetClient() *http.Client {
return c.resty.GetClient()
}
func (c *HTTPClient) chainCallback(newCallback Callback) {
previousCallback := c.callbackChain
if previousCallback == nil {
c.callbackChain = newCallback
return
}
c.callbackChain = func(fn func() (*Response, error)) (*Response, error) {
return newCallback(func() (*Response, error) {
return previousCallback(fn)
})
}
}
func (c *HTTPClient) setTransport(transport http.RoundTripper) {
c.resty.SetTransport(transport)
}
func NewDefaultTransport(transportTimeout time.Duration) http.RoundTripper {
return &Transport{
RoundTripper: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: transportTimeout,
KeepAlive: 15 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ClientSessionCache: tls.NewLRUClientSessionCache(-1),
},
},
}
}
// WithDefaultTransport sets a custom connection timeout to http.Transport.
// This timeout limits the time spent establishing a TCP connection.
//
// More information about timeout: net.Dialer.
func WithDefaultTransport(transportTimeout time.Duration) func(*HTTPClient) {
return func(client *HTTPClient) {
transport := NewDefaultTransport(transportTimeout)
client.setTransport(transport)
}
}
// WithTransport configures the client to use a custom *http.Transport
// More information about transport: [net/http.Transport]
func WithTransport(transport *http.Transport) func(*HTTPClient) {
return func(client *HTTPClient) {
client.setTransport(transport)
}
}
// WithOAUTHTransport allows the client to make OAuth HTTP requests with custom timeout.
// This timeout limits the time spent establishing a TCP connection.
//
// The oauth2.Transport adds an Authorization header with a token
// using clientcredentials.Config information.
//
// More information about timeout: net.Dialer.
//
// More information about the fields used to create the token: clientcredentials.Config.
func WithOAUTHTransport(conf cc.Config, transportTimeout time.Duration) func(*HTTPClient) {
return func(client *HTTPClient) {
transport := &oauth2.Transport{
Source: conf.TokenSource(context.Background()),
Base: NewDefaultTransport(transportTimeout),
}
client.setTransport(transport)
}
}
// WithDefaultTransportWithProxy sets a custom url to use as a proxy to requests.
// The proxyURL is used in the Proxy field. This field specifies a function
// to return a proxy for a given request.
//
// More information about proxy: http.Transport.
func WithDefaultTransportWithProxy(proxyURL *url.URL) func(*HTTPClient) {
return func(client *HTTPClient) {
transport := &Transport{
RoundTripper: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
DialContext: (&net.Dialer{
KeepAlive: 5 * time.Minute,
DualStack: true,
}).DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
ClientSessionCache: tls.NewLRUClientSessionCache(-1),
},
},
}
client.setTransport(transport)
}
}
// WithTimeout encapsulates the resty library to set a custom request timeout.
//
// More information about this feature: https://github.com/go-resty/resty/tree/v1.x
func WithTimeout(timeout time.Duration) func(*HTTPClient) {
return func(client *HTTPClient) {
client.resty.SetTimeout(timeout)
}
}
// WithUserAgent encapsulates the resty library to set a custom user agent to requests.
//
// More information about this feature: https://github.com/go-resty/resty/tree/v1.x
func WithUserAgent(userAgent string) func(*HTTPClient) {
return func(client *HTTPClient) {
client.resty.SetHeader("User-Agent", userAgent)
}
}
// WithBasicAuth encapsulates the resty library to provide basic authentication.
//
// More information about this feature: https://github.com/go-resty/resty/tree/v1.x
func WithBasicAuth(username, password string) func(*HTTPClient) {
return func(client *HTTPClient) {
client.resty.SetBasicAuth(username, password)
}
}
// WithAuthToken encapsulates the resty library to provide token authentication.
//
// More information about this feature: https://github.com/go-resty/resty/tree/v1.x
func WithAuthToken(token string) func(*HTTPClient) {
return func(client *HTTPClient) {
client.resty.SetAuthToken(token)
}
}
// WithCookie encapsulates the resty library to set a cookie to client instance.
//
// More information about this feature: https://github.com/go-resty/resty/tree/v1.x
func WithCookie(name, value string) func(*HTTPClient) {
return func(client *HTTPClient) {
c := http.Cookie{Name: name, Value: value, MaxAge: 3600}
client.resty.SetCookie(&c)
}
}
// WithHostURL encapsulates the resty library to set a host url.
//
// More information about this feature: https://github.com/go-resty/resty/tree/v1.x
func WithHostURL(baseURL string) func(*HTTPClient) {
return func(client *HTTPClient) {
client.hostURL, _ = url.Parse(baseURL)
client.resty.SetBaseURL(baseURL)
}
}
// WithCircuitBreaker enables circuit breaker strategy based on circuitbreaker.Config.
// This functionality relies on https://github.com/slok/goresilience/tree/master/circuitbreaker library.
//
// The config fields are:
// ErrorPercentThresholdToOpen int
// MinimumRequestToOpen int
// SuccessfulRequiredOnHalfOpen int
// WaitDurationInOpenState time.Duration
// MetricsSlidingWindowBucketQuantity int
// MetricsBucketDuration time.Duration
//
// More information about circuitbreaker config: circuitbreaker.Config
func WithCircuitBreaker(config circuitbreaker.Config) func(*HTTPClient) {
runner := circuitbreaker.New(config)
circuitBreakerCallback := func(fn func() (*Response, error)) (*Response, error) {
var resp *Response
err := runner.Run(context.Background(), func(ctx context.Context) error {
var err error
resp, err = fn()
return err
})
return resp, err
}
return func(client *HTTPClient) {
client.chainCallback(circuitBreakerCallback)
}
}
func WithLinearBackoff(retries int, waitTime time.Duration) func(*HTTPClient) {
return WithBackoff(retries, waitTime, false)
}
func WithExponentialBackoff(retries int, waitTime time.Duration) func(*HTTPClient) {
return WithBackoff(retries, waitTime, true)
}
// WithBackoff sets a retry strategy based on its configuration.
// This functionality relies on:
//
// https://github.com/slok/goresilience/tree/master/circuitbreaker
// https://github.com/go-resty/resty/tree/v1.x
//
// Parameters:
//
// retries: is used to set the number of retries after an error occurred.
// waitTime: is the amount of time to wait for a new retry.
// exponential: this field is used to specify which kind of backoff is used.
func WithBackoff(retries int, waitTime time.Duration, exponential bool) func(*HTTPClient) {
r := retry.New(retry.Config{
WaitBase: waitTime,
DisableBackoff: !exponential,
Times: retries,
})
backoffCallback := func(fn func() (*Response, error)) (*Response, error) {
var resp *Response
err := r.Run(context.Background(), func(ctx context.Context) error {
var err error
resp, err = fn()
return err
})
return resp, err
}
return func(client *HTTPClient) {
client.resty.SetRetryCount(retries)
client.chainCallback(backoffCallback)
}
}
// WithMetrics creates a layer to facilitate the metrics use.
//
// Metrics interface implements
// IncrCounter(name string)
// PushToSeries(name string, value float64)
func WithMetrics(m Metrics) func(*HTTPClient) {
return func(client *HTTPClient) {
client.metrics = m
}
}
// WithProxy encapsulates the resty library to set a proxy URL and port.
//
// More information about this feature: https://github.com/go-resty/resty/tree/v1.x
func WithProxy(proxyAddress string) func(*HTTPClient) {
return func(client *HTTPClient) {
client.resty.SetProxy(proxyAddress)
}
}
// WithRetries sets a retry strategy based on its configuration.
// This functionality relies on:
//
// https://github.com/go-resty/resty/tree/v1.x
//
// Parameters:
//
// retries: is used to set the number of retries after an error occurred.
// waitTime: is the amount of time to wait for a new retry.
// maxWaitTime: is the MAX amount of time to wait for a new retry.
func WithRetries(retries int, waitTime time.Duration, maxWaitTime time.Duration) func(*HTTPClient) {
return func(client *HTTPClient) {
client.resty.SetRetryCount(retries)
client.resty.SetRetryWaitTime(waitTime)
client.resty.SetRetryMaxWaitTime(maxWaitTime)
}
}
// WithRetryConditions sets conditions to retry strategy. The conditions will be
// checked for a new retry.
// This functionality relies on:
//
// https://github.com/go-resty/resty/tree/v1.x
//
// More information about conditions: resty.RetryConditionFunc
func WithRetryConditions(conditions ...resty.RetryConditionFunc) func(*HTTPClient) {
return func(client *HTTPClient) {
for _, condition := range conditions {
client.resty.AddRetryCondition(condition)
}
}
}
// WithChainCallback provides a callback functionality that takes as input a Callback type.
func WithChainCallback(fn Callback) func(*HTTPClient) {
return func(client *HTTPClient) {
client.chainCallback(fn)
}
}
func noopCallback(fn func() (*Response, error)) (*Response, error) {
return fn()
}