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