github.com/go-playground/pkg/v5@v5.29.1/net/http/retryable.go (about) 1 //go:build go1.18 2 // +build go1.18 3 4 package httpext 5 6 import ( 7 "context" 8 "fmt" 9 "net/http" 10 "strconv" 11 12 bytesext "github.com/go-playground/pkg/v5/bytes" 13 errorsext "github.com/go-playground/pkg/v5/errors" 14 . "github.com/go-playground/pkg/v5/values/result" 15 ) 16 17 var ( 18 // retryableStatusCodes defines the common HTTP response codes that are considered retryable. 19 retryableStatusCodes = map[int]bool{ 20 http.StatusServiceUnavailable: true, 21 http.StatusTooManyRequests: true, 22 http.StatusBadGateway: true, 23 http.StatusGatewayTimeout: true, 24 http.StatusRequestTimeout: true, 25 26 // 524 is a Cloudflare specific error which indicates it connected to the origin server but did not receive 27 // response within 100 seconds and so times out. 28 // https://support.cloudflare.com/hc/en-us/articles/115003011431-Error-524-A-timeout-occurred#524error 29 524: true, 30 } 31 // nonRetryableStatusCodes defines common HTTP responses that are not considered never to be retryable. 32 nonRetryableStatusCodes = map[int]bool{ 33 http.StatusBadRequest: true, 34 http.StatusUnauthorized: true, 35 http.StatusForbidden: true, 36 http.StatusNotFound: true, 37 http.StatusMethodNotAllowed: true, 38 http.StatusNotAcceptable: true, 39 http.StatusProxyAuthRequired: true, 40 http.StatusConflict: true, 41 http.StatusLengthRequired: true, 42 http.StatusPreconditionFailed: true, 43 http.StatusRequestEntityTooLarge: true, 44 http.StatusRequestURITooLong: true, 45 http.StatusUnsupportedMediaType: true, 46 http.StatusRequestedRangeNotSatisfiable: true, 47 http.StatusExpectationFailed: true, 48 http.StatusTeapot: true, 49 http.StatusMisdirectedRequest: true, 50 http.StatusUnprocessableEntity: true, 51 http.StatusPreconditionRequired: true, 52 http.StatusRequestHeaderFieldsTooLarge: true, 53 http.StatusUnavailableForLegalReasons: true, 54 http.StatusNotImplemented: true, 55 http.StatusHTTPVersionNotSupported: true, 56 http.StatusLoopDetected: true, 57 http.StatusNotExtended: true, 58 http.StatusNetworkAuthenticationRequired: true, 59 } 60 ) 61 62 // ErrRetryableStatusCode can be used to indicate a retryable HTTP status code was encountered as an error. 63 type ErrRetryableStatusCode struct { 64 Response *http.Response 65 } 66 67 func (e ErrRetryableStatusCode) Error() string { 68 return fmt.Sprintf("retryable HTTP status code encountered: %d", e.Response.StatusCode) 69 } 70 71 // ErrUnexpectedResponse can be used to indicate an unexpected response was encountered as an error and provide access to the *http.Response. 72 type ErrUnexpectedResponse struct { 73 Response *http.Response 74 } 75 76 func (e ErrUnexpectedResponse) Error() string { 77 return "unexpected response encountered" 78 } 79 80 // IsRetryableStatusCode returns true if the provided status code is considered retryable. 81 func IsRetryableStatusCode(code int) bool { 82 return retryableStatusCodes[code] 83 } 84 85 // IsNonRetryableStatusCode returns true if the provided status code should generally not be retryable. 86 func IsNonRetryableStatusCode(code int) bool { 87 return nonRetryableStatusCodes[code] 88 } 89 90 // BuildRequestFn is a function used to rebuild an HTTP request for use in retryable code. 91 type BuildRequestFn func(ctx context.Context) (*http.Request, error) 92 93 // IsRetryableStatusCodeFn is a function used to determine if the provided status code is considered retryable. 94 type IsRetryableStatusCodeFn func(code int) bool 95 96 // DoRetryableResponse will execute the provided functions code and automatically retry before returning the *http.Response. 97 // 98 // Deprecated: use `httpext.Retrier` instead which corrects design issues with the current implementation. 99 func DoRetryableResponse(ctx context.Context, onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, buildFn BuildRequestFn) Result[*http.Response, error] { 100 if client == nil { 101 client = http.DefaultClient 102 } 103 var attempt int 104 for { 105 req, err := buildFn(ctx) 106 if err != nil { 107 return Err[*http.Response, error](err) 108 } 109 110 resp, err := client.Do(req) 111 if err != nil { 112 if retryReason, isRetryable := errorsext.IsRetryableHTTP(err); isRetryable { 113 opt := onRetryFn(ctx, err, retryReason, attempt) 114 if opt.IsSome() { 115 return Err[*http.Response, error](opt.Unwrap()) 116 } 117 attempt++ 118 continue 119 } 120 return Err[*http.Response, error](err) 121 } 122 123 if isRetryableStatusCode(resp.StatusCode) { 124 opt := onRetryFn(ctx, ErrRetryableStatusCode{Response: resp}, strconv.Itoa(resp.StatusCode), attempt) 125 if opt.IsSome() { 126 return Err[*http.Response, error](opt.Unwrap()) 127 } 128 attempt++ 129 continue 130 } 131 return Ok[*http.Response, error](resp) 132 } 133 } 134 135 // DoRetryable will execute the provided functions code and automatically retry before returning the result. 136 // 137 // This function currently supports decoding the following automatically based on the response Content-Type with 138 // Gzip supported: 139 // - JSON 140 // - XML 141 // 142 // Deprecated: use `httpext.Retrier` instead which corrects design issues with the current implementation. 143 func DoRetryable[T any](ctx context.Context, isRetryableFn errorsext.IsRetryableFn[error], onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, expectedResponseCode int, maxMemory bytesext.Bytes, buildFn BuildRequestFn) Result[T, error] { 144 145 return errorsext.DoRetryable(ctx, isRetryableFn, onRetryFn, func(ctx context.Context) Result[T, error] { 146 147 result := DoRetryableResponse(ctx, onRetryFn, isRetryableStatusCode, client, buildFn) 148 if result.IsErr() { 149 return Err[T, error](result.Err()) 150 } 151 resp := result.Unwrap() 152 153 if resp.StatusCode != expectedResponseCode { 154 return Err[T, error](ErrUnexpectedResponse{Response: resp}) 155 } 156 defer resp.Body.Close() 157 158 data, err := DecodeResponse[T](resp, maxMemory) 159 if err != nil { 160 return Err[T, error](err) 161 } 162 return Ok[T, error](data) 163 }) 164 }