go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/lhttp/client.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package lhttp 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "net/http" 22 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/common/logging" 25 "go.chromium.org/luci/common/retry" 26 "go.chromium.org/luci/common/retry/transient" 27 ) 28 29 // Handler is called once or multiple times for each HTTP request that is tried. 30 type Handler func(*http.Response) error 31 32 // ErrorHandler is called once or multiple times for each HTTP request that is 33 // tried. It is called when any non-200 response code is received, or if some 34 // other network error occurs. 35 // resp may be nil if a network error occurred before the response was received. 36 // The ErrorHandler must close the provided resp, if any. 37 // Return the same error again to continue retry behaviour, or nil to pretend 38 // this error was a success. 39 type ErrorHandler func(resp *http.Response, err error) error 40 41 // RequestGen is a generator function to create a new request. It may be called 42 // multiple times if an operation needs to be retried. The HTTP server is 43 // responsible for closing the Request body, as per http.Request Body method 44 // documentation. 45 type RequestGen func() (*http.Request, error) 46 47 var httpTagKey = errors.NewTagKey("this is an HTTP error") 48 49 func applyHTTPTag(err error, status int) error { 50 return errors.TagValue{Key: httpTagKey, Value: status}.Apply(err) 51 } 52 53 func IsHTTPError(err error) (status int, ok bool) { 54 d, ok := errors.TagValueIn(httpTagKey, err) 55 if ok { 56 status = d.(int) 57 } 58 return 59 } 60 61 // NewRequest returns a retriable request. 62 // 63 // The handler func is responsible for closing the response Body before 64 // returning. It should return retry.Error in case of retriable error, for 65 // example if a TCP connection is terminated while receiving the content. 66 // 67 // If rFn is nil, NewRequest will use a default exponential backoff strategy 68 // only for transient errors. 69 // 70 // If errorHandler is nil, the default error handler will drain and close the 71 // response body. 72 func NewRequest(ctx context.Context, c *http.Client, rFn retry.Factory, rgen RequestGen, 73 handler Handler, errorHandler ErrorHandler) func() (int, error) { 74 if rFn == nil { 75 rFn = transient.Only(retry.Default) 76 } 77 if errorHandler == nil { 78 errorHandler = func(resp *http.Response, err error) error { 79 if resp != nil { 80 // Drain and close the resp.Body. 81 io.Copy(io.Discard, resp.Body) 82 resp.Body.Close() 83 } 84 return err 85 } 86 } 87 88 return func() (int, error) { 89 status, attempts := 0, 0 90 err := retry.Retry(ctx, rFn, func() error { 91 attempts++ 92 req, err := rgen() 93 if err != nil { 94 return errors.Annotate(err, "failed to call rgen").Err() 95 } 96 97 resp, err := c.Do(req) 98 if err != nil { 99 logging.Debugf(ctx, "failed to call c.Do: %v", err) 100 err = errors.Annotate(err, "failed to call c.Do").Err() 101 // Retry every error. This is sad when you specify an invalid hostname but 102 // it's better than failing when DNS resolution is flaky. 103 return errorHandler(nil, transient.Tag.Apply(err)) 104 } 105 status = resp.StatusCode 106 107 switch { 108 case status == 408, status == 429, status >= 500: 109 // The HTTP status code means the request should be retried. 110 err = errors.Reason("http request failed: %s (HTTP %d)", http.StatusText(status), status). 111 Tag(transient.Tag).Err() 112 case status >= 400: 113 // Any other failure code is a hard failure. 114 err = fmt.Errorf("http request failed: %s (HTTP %d)", http.StatusText(status), status) 115 default: 116 // The handler may still return a retry.Error to indicate that the request 117 // should be retried even on successful status code. 118 err = handler(resp) 119 if err != nil { 120 return errors.Annotate(err, "failed to handle response").Err() 121 } 122 return err 123 } 124 125 err = applyHTTPTag(err, status) 126 return errorHandler(resp, err) 127 }, nil) 128 if err != nil { 129 err = errors.Annotate(err, "gave up after %d attempts", attempts).Err() 130 } 131 return status, err 132 } 133 }