-
Notifications
You must be signed in to change notification settings - Fork 1
/
http_client_wrapper.go
131 lines (111 loc) · 3.4 KB
/
http_client_wrapper.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
package shared
import (
"bytes"
"io"
"net/http"
"net/url"
"time"
"github.com/avast/retry-go/v4"
"github.com/pkg/errors"
)
type HTTPClientWrapper interface {
ExecuteRequest(path, method string, body []byte) (*http.Response, error)
}
type httpClientWrapper struct {
httpClient *http.Client
baseURL string
headers map[string]string
torProxyURL string
retry uint
}
type HTTPResponseError struct {
error
StatusCode int
}
func BuildResponseError(statusCode int, err error) error {
return HTTPResponseError{StatusCode: statusCode, error: err}
}
func NewHTTPClientWrapper(baseURL, torProxyURL string, timeout time.Duration, headers map[string]string, addJSONHeaders bool, opts ...HTTPClientWrapperOption) (HTTPClientWrapper, error) {
if headers == nil {
headers = map[string]string{}
}
if addJSONHeaders {
headers["Accept"] = "application/json"
headers["Content-Type"] = "application/json"
}
client := &http.Client{
Timeout: timeout,
}
if torProxyURL != "" {
proxyURL, err := url.Parse(torProxyURL)
if err != nil {
return nil, errors.Wrap(err, "failed to parse Tor proxy URL")
}
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
}
params := defaultHTTPClientWrapperOptions
for _, opt := range opts {
opt(¶ms)
}
return &httpClientWrapper{
httpClient: client,
baseURL: baseURL,
headers: headers,
torProxyURL: torProxyURL,
retry: params.Retry,
}, nil
}
type HTTPClientWrapperOptions struct {
Retry uint
}
var defaultHTTPClientWrapperOptions = HTTPClientWrapperOptions{
Retry: 5,
}
type HTTPClientWrapperOption func(*HTTPClientWrapperOptions)
func WithRetry(retry uint) HTTPClientWrapperOption {
return func(opts *HTTPClientWrapperOptions) {
opts.Retry = retry
}
}
// ExecuteRequest calls an endpoint, optional body and error handling. path is appended to the baseURL, same for json
// headers and authorization. If request results in non 2xx response, will always return error with payload body in err message.
// response should have defer response.Body.Close() after the error check as it could be nil when err is != nil
func (h httpClientWrapper) ExecuteRequest(path, method string, body []byte) (*http.Response, error) {
req, err := http.NewRequest(method, h.baseURL+path, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
for hk, hv := range h.headers {
req.Header.Set(hk, hv)
}
var res *http.Response
err = retry.Do(func() error {
res, err = h.httpClient.Do(req)
// handle error status codes
if err == nil && res != nil && res.StatusCode > 299 {
defer res.Body.Close() //nolint
body, err := io.ReadAll(res.Body)
if err != nil {
return errors.Wrapf(err, "error reading failed request body")
}
errResp := BuildResponseError(res.StatusCode, errors.Errorf("received non success status code %d for url %s with body: %s", res.StatusCode, req.URL.String(), string(body)))
if code := res.StatusCode; 400 <= code && code < 500 {
// unrecoverable since probably bad payload or n
return retry.Unrecoverable(BuildResponseError(code, errResp))
}
return errResp
}
return err
}, retry.Attempts(h.retry), retry.Delay(500*time.Millisecond), retry.MaxDelay(9*time.Second))
if res == nil {
return nil, err
}
if retryErrors, ok := err.(retry.Error); ok {
for _, e := range retryErrors {
if httpError, ok := e.(HTTPResponseError); ok {
return res, httpError
}
}
}
return res, err
}