github.com/searKing/golang/go@v1.2.74/net/http/backoff.go (about) 1 // Copyright 2022 The searKing Author. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package http 6 7 import ( 8 "bytes" 9 "context" 10 "crypto/x509" 11 "encoding/json" 12 "fmt" 13 "io" 14 "net/http" 15 "net/url" 16 "regexp" 17 "strconv" 18 "strings" 19 "time" 20 21 time_ "github.com/searKing/golang/go/time" 22 ) 23 24 var ( 25 // A regular expression to match the error returned by net/http when the 26 // configured number of redirects is exhausted. This error isn't typed 27 // specifically so we resort to matching on the error string. 28 redirectsErrorRe = regexp.MustCompile(`stopped after \d+ redirects\z`) 29 30 // A regular expression to match the error returned by net/http when the 31 // scheme specified in the URL is invalid. This error isn't typed 32 // specifically so we resort to matching on the error string. 33 schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`) 34 ) 35 36 // RetryAfter tries to parse Retry-After response header when a http.StatusTooManyRequests 37 // (HTTP Code 429) is found in the resp parameter. Hence, it will return the number of 38 // seconds the server states it may be ready to process more requests from this client. 39 // Don't retry if the error was due to too many redirects. 40 // Don't retry if the error was due to an invalid protocol scheme. 41 // Don't retry if the error was due to TLS cert verification failure. 42 // Don't retry if the http's StatusCode is http.StatusNotImplemented. 43 func RetryAfter(resp *http.Response, err error, defaultBackoff time.Duration) (backoff time.Duration, retry bool) { 44 backoff = defaultBackoff 45 if resp != nil { 46 if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode == http.StatusServiceUnavailable { 47 if sleepInSeconds, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64); err == nil { 48 backoff = time.Second * time.Duration(sleepInSeconds) 49 } 50 } 51 } 52 53 if err != nil { 54 if v, ok := err.(*url.Error); ok { 55 // Don't retry if the error was due to too many redirects. 56 if redirectsErrorRe.MatchString(v.Error()) { 57 return backoff, false 58 } 59 60 // Don't retry if the error was due to an invalid protocol scheme. 61 if schemeErrorRe.MatchString(v.Error()) { 62 return backoff, false 63 } 64 65 // Don't retry if the error was due to TLS cert verification failure. 66 if _, ok := v.Err.(x509.UnknownAuthorityError); ok { 67 return backoff, false 68 } 69 } 70 71 // The error is likely recoverable so retry. 72 return backoff, true 73 } 74 75 if resp != nil { 76 // 429 Too Many Requests is recoverable. Sometimes the server puts 77 // a Retry-After response header to indicate when the server is 78 // available to start processing request from client. 79 if resp.StatusCode == http.StatusTooManyRequests { 80 return backoff, true 81 } 82 83 // Check the response code. We retry on 500-range responses to allow 84 // the server time to recover, as 500's are typically not permanent 85 // errors and may relate to outages on the server side. This will catch 86 // invalid response codes as well, like 0 and 999. 87 if resp.StatusCode == 0 || (resp.StatusCode >= http.StatusInternalServerError && resp.StatusCode != http.StatusNotImplemented) { 88 return backoff, true 89 } 90 } 91 return backoff, false 92 } 93 94 // ReplaceHttpRequestBody replace Body and recalculate ContentLength 95 // If ContentLength should not be recalculated, save and restore it after ReplaceHttpRequestBody 96 func ReplaceHttpRequestBody(req *http.Request, body io.Reader) { 97 if req.Body != nil { 98 req.Body.Close() 99 } 100 rc, ok := body.(io.ReadCloser) 101 if !ok && body != nil { 102 rc = io.NopCloser(body) 103 } 104 req.Body = rc 105 req.ContentLength = 0 106 if body != nil { 107 switch v := body.(type) { 108 case *bytes.Buffer: 109 req.ContentLength = int64(v.Len()) 110 buf := v.Bytes() 111 req.GetBody = func() (io.ReadCloser, error) { 112 r := bytes.NewReader(buf) 113 return io.NopCloser(r), nil 114 } 115 case *bytes.Reader: 116 req.ContentLength = int64(v.Len()) 117 snapshot := *v 118 req.GetBody = func() (io.ReadCloser, error) { 119 r := snapshot 120 return io.NopCloser(&r), nil 121 } 122 case *strings.Reader: 123 req.ContentLength = int64(v.Len()) 124 snapshot := *v 125 req.GetBody = func() (io.ReadCloser, error) { 126 r := snapshot 127 return io.NopCloser(&r), nil 128 } 129 default: 130 // This is where we'd set it to -1 (at least 131 // if body != NoBody) to mean unknown, but 132 // that broke people during the Go 1.8 testing 133 // period. People depend on it being 0 I 134 // guess. Maybe retry later. See Issue 18117. 135 } 136 // For client requests, Request.ContentLength of 0 137 // means either actually 0, or unknown. The only way 138 // to explicitly say that the ContentLength is zero is 139 // to set the Body to nil. But turns out too much code 140 // depends on NewRequest returning a non-nil Body, 141 // so we use a well-known ReadCloser variable instead 142 // and have the http package also treat that sentinel 143 // variable to mean explicitly zero. 144 if req.GetBody != nil && req.ContentLength == 0 { 145 req.Body = http.NoBody 146 req.GetBody = func() (io.ReadCloser, error) { return http.NoBody, nil } 147 } 148 } 149 } 150 151 // ClientInvoker is called by ClientInterceptor to complete RPCs. 152 type ClientInvoker func(req *http.Request, retry int) (*http.Response, error) 153 154 // ClientInterceptor intercepts the execution of a HTTP on the client. 155 // interceptors can be specified as a DoWithBackoffOption, using 156 // WithClientInterceptor() or WithChainClientInterceptor(), when DoWithBackoffOption. 157 // When a interceptor(s) is set, gRPC delegates all http invocations to the interceptor, 158 // and it is the responsibility of the interceptor to call invoker to complete the processing 159 // of the HTTP. 160 type ClientInterceptor func(req *http.Request, retry int, invoker ClientInvoker, opts ...DoWithBackoffOption) (resp *http.Response, err error) 161 162 type RetryAfterHandler func(resp *http.Response, err error, defaultBackoff time.Duration) (backoff time.Duration, retry bool) 163 164 // DoRetryHandler send an HTTP request with retry seq and returns an HTTP response, following 165 // policy (such as redirects, cookies, auth) as configured on the 166 // client. 167 type DoRetryHandler = ClientInvoker 168 169 var DefaultClientDoRetryHandler = func(req *http.Request, retry int) (*http.Response, error) { 170 return http.DefaultClient.Do(req) 171 } 172 173 var DefaultTransportDoRetryHandler = func(req *http.Request, retry int) (*http.Response, error) { 174 return http.DefaultTransport.RoundTrip(req) 175 } 176 177 //go:generate go-option -type "doWithBackoff" 178 type doWithBackoff struct { 179 DoRetryHandler DoRetryHandler 180 clientInterceptor ClientInterceptor 181 ChainClientInterceptors []ClientInterceptor 182 RetryAfter RetryAfterHandler 183 ExponentialBackOffOption []time_.ExponentialBackOffOption 184 } 185 186 func (o *doWithBackoff) SetDefault() { 187 o.DoRetryHandler = DefaultClientDoRetryHandler 188 o.RetryAfter = RetryAfter 189 } 190 191 // getClientInvoker recursively generate the chained client invoker. 192 func getClientInvoker(interceptors []ClientInterceptor, curr int, finalInvoker ClientInvoker, opts ...DoWithBackoffOption) ClientInvoker { 193 if curr == len(interceptors)-1 { 194 return finalInvoker 195 } 196 return func(req *http.Request, retry int) (*http.Response, error) { 197 return interceptors[curr+1](req, retry, getClientInvoker(interceptors, curr+1, finalInvoker), opts...) 198 } 199 } 200 201 func (o *doWithBackoff) Complete() { 202 if o.DoRetryHandler == nil { 203 o.DoRetryHandler = DefaultClientDoRetryHandler 204 } 205 interceptors := o.ChainClientInterceptors 206 o.ChainClientInterceptors = nil 207 // Prepend o.ClientInterceptor to the chaining interceptors if it exists, since ClientInterceptor will 208 // be executed before any other chained interceptors. 209 if o.clientInterceptor != nil { 210 interceptors = append([]ClientInterceptor{o.clientInterceptor}, interceptors...) 211 } 212 var chainedInt ClientInterceptor 213 if len(interceptors) == 0 { 214 chainedInt = nil 215 } else if len(interceptors) == 1 { 216 chainedInt = interceptors[0] 217 } else { 218 chainedInt = func(req *http.Request, retry int, invoker ClientInvoker, opts ...DoWithBackoffOption) (resp *http.Response, err error) { 219 return interceptors[0](req, retry, getClientInvoker(interceptors, 0, invoker), opts...) 220 } 221 } 222 o.clientInterceptor = chainedInt 223 } 224 225 // DoWithBackoff will retry by exponential backoff if failed. 226 // If request is not rewindable, retry wil be skipped. 227 func DoWithBackoff(httpReq *http.Request, opts ...DoWithBackoffOption) (*http.Response, error) { 228 var opt doWithBackoff 229 opt.SetDefault() 230 opt.ApplyOptions(opts...) 231 if opt.RetryAfter == nil { 232 opt.RetryAfter = RetryAfter 233 } 234 opt.Complete() 235 236 var option []time_.ExponentialBackOffOption 237 option = append(option, time_.WithExponentialBackOffOptionMaxElapsedCount(3)) 238 option = append(option, opt.ExponentialBackOffOption...) 239 backoff := time_.NewDefaultExponentialBackOff(option...) 240 rewindableErr := RequestWithBodyRewindable(httpReq) 241 var retries int 242 for { 243 if retries > 0 && httpReq.GetBody != nil { 244 newBody, err := httpReq.GetBody() 245 if err != nil { 246 return nil, err 247 } 248 httpReq.Body = newBody 249 } 250 var do = opt.DoRetryHandler 251 httpDo := do 252 if opt.clientInterceptor != nil { 253 httpDo = func(req *http.Request, retry int) (*http.Response, error) { 254 return opt.clientInterceptor(req, retry, do, opts...) 255 } 256 } 257 resp, err := httpDo(httpReq, retries) 258 259 wait, ok := backoff.NextBackOff() 260 if !ok { 261 if err != nil { 262 return nil, fmt.Errorf("http do reach backoff limit after retries %d: %w", retries, err) 263 } else { 264 return resp, nil 265 } 266 } 267 268 wait, retry := opt.RetryAfter(resp, err, wait) 269 if !retry { 270 if err != nil { 271 return nil, fmt.Errorf("http do reach server limit after retries %d: %w", retries, err) 272 } else { 273 return resp, nil 274 } 275 } 276 277 if rewindableErr != nil { 278 if err != nil { 279 return nil, fmt.Errorf("http do cannot rewindbody after retries %d: %w", retries, err) 280 } else { 281 return resp, nil 282 } 283 } 284 285 timer := time.NewTimer(wait) 286 select { 287 case <-timer.C: 288 retries++ 289 continue 290 case <-httpReq.Context().Done(): 291 timer.Stop() 292 if err != nil { 293 return nil, fmt.Errorf("http do canceled after retries %d: %w", retries, err) 294 } else { 295 return resp, nil 296 } 297 } 298 } 299 } 300 301 func HeadWithBackoff(ctx context.Context, url string, opts ...DoWithBackoffOption) (*http.Response, error) { 302 req, err := http.NewRequest(http.MethodHead, url, nil) 303 req = req.WithContext(ctx) 304 if err != nil { 305 return nil, err 306 } 307 return DoWithBackoff(req, opts...) 308 } 309 310 func GetWithBackoff(ctx context.Context, url string, opts ...DoWithBackoffOption) (*http.Response, error) { 311 req, err := http.NewRequest(http.MethodGet, url, nil) 312 req = req.WithContext(ctx) 313 if err != nil { 314 return nil, err 315 } 316 return DoWithBackoff(req, opts...) 317 } 318 319 func PostWithBackoff(ctx context.Context, url, contentType string, body io.Reader, opts ...DoWithBackoffOption) (resp *http.Response, err error) { 320 req, err := http.NewRequest(http.MethodPost, url, body) 321 req = req.WithContext(ctx) 322 if err != nil { 323 return nil, err 324 } 325 if contentType != "" { 326 req.Header.Set("Content-Type", contentType) 327 } 328 return DoWithBackoff(req, opts...) 329 } 330 331 func PostFormWithBackoff(ctx context.Context, url string, data url.Values, opts ...DoWithBackoffOption) (resp *http.Response, err error) { 332 return PostWithBackoff(ctx, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()), opts...) 333 } 334 335 func PutWithBackoff(ctx context.Context, url, contentType string, body io.Reader, opts ...DoWithBackoffOption) (resp *http.Response, err error) { 336 req, err := http.NewRequest(http.MethodPut, url, body) 337 req = req.WithContext(ctx) 338 if err != nil { 339 return nil, err 340 } 341 if contentType != "" { 342 req.Header.Set("Content-Type", contentType) 343 } 344 return DoWithBackoff(req, opts...) 345 } 346 347 // DoJson the same as HttpDo, but bind with json 348 func DoJson(httpReq *http.Request, req, resp interface{}) error { 349 if req != nil { 350 data, err := json.Marshal(req) 351 if err != nil { 352 return err 353 } 354 reqBody := bytes.NewReader(data) 355 httpReq.Header.Set("Content-Type", "application/json") 356 ReplaceHttpRequestBody(httpReq, reqBody) 357 } 358 359 httpResp, err := DefaultClientDoRetryHandler(httpReq, 0) 360 if err != nil { 361 return err 362 } 363 defer httpResp.Body.Close() 364 if resp == nil { 365 return nil 366 } 367 368 body, err := io.ReadAll(httpResp.Body) 369 if err != nil { 370 return err 371 } 372 373 return json.Unmarshal(body, resp) 374 } 375 376 // DoJsonWithBackoff the same as DoWithBackoff, but bind with json 377 func DoJsonWithBackoff(httpReq *http.Request, req, resp interface{}, opts ...DoWithBackoffOption) error { 378 if req != nil { 379 data, err := json.Marshal(req) 380 if err != nil { 381 return err 382 } 383 reqBody := bytes.NewReader(data) 384 httpReq.Header.Set("Content-Type", "application/json") 385 ReplaceHttpRequestBody(httpReq, reqBody) 386 } 387 httpResp, err := DoWithBackoff(httpReq, opts...) 388 389 if err != nil { 390 return err 391 } 392 defer httpResp.Body.Close() 393 if resp == nil { 394 return nil 395 } 396 397 body, err := io.ReadAll(httpResp.Body) 398 if err != nil { 399 return err 400 } 401 return json.Unmarshal(body, resp) 402 }