-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathclient.go
215 lines (179 loc) · 7.1 KB
/
client.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
package retryhttp
import (
"context"
"crypto/x509"
"errors"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/ActiveState/cli/internal/constants"
"github.com/ActiveState/cli/internal/logging"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-retryablehttp"
"github.com/thoas/go-funk"
"github.com/ActiveState/cli/internal/condition"
"github.com/ActiveState/cli/internal/locale"
"github.com/ActiveState/cli/internal/multilog"
)
type UserNetworkError struct {
_testCode int // used for tests
}
func (e *UserNetworkError) Error() string {
return "network error"
}
func (e *UserNetworkError) ExitCode() int {
return 11
}
type Logger interface {
Printf(string, ...interface{})
}
var (
DefaultTimeout = time.Second * 30
DefaultRetries = 5
DefaultClient = NewClient(DefaultTimeout, DefaultRetries)
// A regular expression to match the error returned by net/http when the
// configured number of redirects is exhausted. This error isn't typed
// specifically so we resort to matching on the error string.
redirectsErrorRe = regexp.MustCompile(`stopped after \d+ redirects\z`)
// A regular expression to match the error returned by net/http when the
// scheme specified in the URL is invalid. This error isn't typed
// specifically so we resort to matching on the error string.
schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`)
retryableStatusCodes = []int{
// 4XX Status codes
// The server timed out waiting for the request from client.
http.StatusRequestTimeout,
// Sometimes the server puts a Retry-After response header
// to indicate when the server is available to start processing
// request from client.
http.StatusTooManyRequests,
// The server is unwilling to risk
// processing a request that might be replayed.
http.StatusTooEarly,
// 5XX Status codes
// The server, while acting as a gateway or proxy, did not receive
// a valid response.
http.StatusBadGateway,
// The server is currently unable to handle the request due to a
// temporary overload or scheduled maintenance, which will likely
// be alleviated after some delay.
http.StatusServiceUnavailable,
// The server, while acting as a gateway or proxy, did not receive
// a timely response from an upstream server it needed to access
// in order to complete the request.
http.StatusGatewayTimeout,
}
)
type Client struct {
*retryablehttp.Client
}
func (c *Client) Get(url string) (*http.Response, error) {
return normalizeResponse(c.Client.Get(url))
}
func (c *Client) Head(url string) (*http.Response, error) {
return normalizeResponse(c.Client.Head(url))
}
func (c *Client) Post(url, bodyType string, body interface{}) (*http.Response, error) {
return normalizeResponse(c.Client.Post(url, bodyType, body))
}
func (c *Client) PostForm(url string, data url.Values) (*http.Response, error) {
return normalizeResponse(c.Client.PostForm(url, data))
}
func (c *Client) Do(req *retryablehttp.Request) (*http.Response, error) {
return normalizeResponse(c.Client.Do(req))
}
func (c *Client) StandardClient() *http.Client {
return &http.Client{
Transport: &RoundTripper{client: c},
}
}
func normalizeResponse(res *http.Response, err error) (*http.Response, error) {
if res != nil {
switch res.StatusCode {
case 408:
return res, locale.WrapExternalError(&UserNetworkError{408}, "err_user_network_server_timeout", "Request failed due to timeout during communication with server. {{.V0}}", locale.Tr("err_user_network_solution", constants.ForumsURL))
case 425:
return res, locale.WrapExternalError(&UserNetworkError{425}, "err_user_network_tooearly", "Request failed due to retrying connection too fast. {{.V0}}", locale.Tr("err_user_network_solution", constants.ForumsURL))
case 429:
return res, locale.WrapExternalError(&UserNetworkError{429}, "err_user_network_toomany", "Request failed due to too many requests. {{.V0}}", locale.Tr("err_user_network_solution", constants.ForumsURL))
}
}
var dnsError *net.DNSError
if errors.As(err, &dnsError) {
return res, locale.WrapExternalError(&UserNetworkError{}, "err_user_network_dns", "Request failed due to DNS error: {{.V0}}. {{.V1}}", err.Error(), locale.Tr("err_user_network_solution", constants.ForumsURL))
}
// Due to Go's handling of these types of errors and due to Windows localizing the errors in question we have to rely on the `wsarecv:` keyword to capture a series
// of user facing network issues. Theoretically this could cause some false positives, but at the time of writing I could not find any instances on rollbar
// where `wsarecv:` was being reported as anything other than a network issue caused by the user or their network
if err != nil && strings.Contains(err.Error(), "wsarecv:") {
multilog.Error("Non-Critical User Network Issue, please vet for false-positive: %v", err) // Logging so we can vet for false positives
return res, locale.WrapError(&UserNetworkError{}, "err_user_network_wsarecv", "Request failed due to user network error: {{.V0}}. {{.V1}}", err.Error(), locale.Tr("err_user_network_solution", constants.ForumsURL))
}
return res, err
}
func normalizeRetryResponse(res *http.Response, err error, numTries int) (*http.Response, error) {
logging.Debug("Retry failed with error: %v, after %d tries", err, numTries)
if err2, ok := err.(net.Error); ok && err2.Timeout() {
return res, locale.WrapExternalError(&UserNetworkError{-1}, "err_user_network_timeout", "", locale.Tr("err_user_network_solution", constants.ForumsURL))
}
return res, err
}
func NewClient(timeout time.Duration, retries int) *Client {
if timeout < 0 {
timeout = DefaultTimeout
}
if retries < 0 {
retries = DefaultRetries
}
retryClient := retryablehttp.NewClient()
retryClient.Logger = nil
// retryClient.Logger = logging.CurrentHandler() // Enable this to get debug info in our logs
retryClient.HTTPClient = &http.Client{
Transport: transport(),
Timeout: timeout,
}
retryClient.RetryMax = retries
retryClient.ErrorHandler = normalizeRetryResponse
retryClient.CheckRetry = retryPolicy
return &Client{
Client: retryClient,
}
}
func transport() http.RoundTripper {
if condition.InUnitTest() {
return http.DefaultTransport
}
return cleanhttp.DefaultPooledTransport()
}
// retryPolicy is a modified version of retryablehttp.DefaultRetryPolicy to handle
// status codes differently.
func retryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
if ctx.Err() != nil {
return false, ctx.Err()
}
if err != nil {
if v, ok := err.(*url.Error); ok {
// Don't retry if the error was due to too many redirects.
if redirectsErrorRe.MatchString(v.Error()) {
return false, nil
}
// Don't retry if the error was due to an invalid protocol scheme.
if schemeErrorRe.MatchString(v.Error()) {
return false, nil
}
// Don't retry if the error was due to TLS cert verification failure.
if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
return false, nil
}
}
// The error is likely recoverable so retry.
return true, err
}
return isRetryableStatus(resp.StatusCode), nil
}
func isRetryableStatus(status int) bool {
return funk.Contains(retryableStatusCodes, status)
}