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
|
package linodego
import (
"encoding/json"
"errors"
"log"
"net/http"
"strconv"
"time"
"golang.org/x/net/http2"
)
const (
// nolint:unused
httpRetryAfterHeaderName = "Retry-After"
// nolint:unused
httpMaintenanceModeHeaderName = "X-Maintenance-Mode"
// nolint:unused
httpDefaultRetryCount = 1000
)
// RetryConditional is a type alias for a function that determines if a request should be retried based on the response and error.
// nolint:unused
type httpRetryConditional func(*http.Response, error) bool
// RetryAfter is a type alias for a function that determines the duration to wait before retrying based on the response.
// nolint:unused
type httpRetryAfter func(*http.Response) (time.Duration, error)
// Configures http.Client to lock until enough time has passed to retry the request as determined by the Retry-After response header.
// If the Retry-After header is not set, we fall back to the value of SetPollDelay.
// nolint:unused
func httpConfigureRetries(c *httpClient) {
c.retryConditionals = append(c.retryConditionals, httpcheckRetryConditionals(c))
c.retryAfter = httpRespectRetryAfter
}
// nolint:unused
func httpcheckRetryConditionals(c *httpClient) httpRetryConditional {
return func(resp *http.Response, err error) bool {
for _, retryConditional := range c.retryConditionals {
retry := retryConditional(resp, err)
if retry {
log.Printf("[INFO] Received error %v - Retrying", err)
return true
}
}
return false
}
}
// nolint:unused
func httpRespectRetryAfter(resp *http.Response) (time.Duration, error) {
retryAfterStr := resp.Header.Get(retryAfterHeaderName)
if retryAfterStr == "" {
return 0, nil
}
retryAfter, err := strconv.Atoi(retryAfterStr)
if err != nil {
return 0, err
}
duration := time.Duration(retryAfter) * time.Second
log.Printf("[INFO] Respecting Retry-After Header of %d (%s)", retryAfter, duration)
return duration, nil
}
// Retry conditions
// nolint:unused
func httpLinodeBusyRetryCondition(resp *http.Response, _ error) bool {
apiError, ok := getAPIError(resp)
linodeBusy := ok && apiError.Error() == "Linode busy."
retry := resp.StatusCode == http.StatusBadRequest && linodeBusy
return retry
}
// nolint:unused
func httpTooManyRequestsRetryCondition(resp *http.Response, _ error) bool {
return resp.StatusCode == http.StatusTooManyRequests
}
// nolint:unused
func httpServiceUnavailableRetryCondition(resp *http.Response, _ error) bool {
serviceUnavailable := resp.StatusCode == http.StatusServiceUnavailable
// During maintenance events, the API will return a 503 and add
// an `X-MAINTENANCE-MODE` header. Don't retry during maintenance
// events, only for legitimate 503s.
if serviceUnavailable && resp.Header.Get(maintenanceModeHeaderName) != "" {
log.Printf("[INFO] Linode API is under maintenance, request will not be retried - please see status.linode.com for more information")
return false
}
return serviceUnavailable
}
// nolint:unused
func httpRequestTimeoutRetryCondition(resp *http.Response, _ error) bool {
return resp.StatusCode == http.StatusRequestTimeout
}
// nolint:unused
func httpRequestGOAWAYRetryCondition(_ *http.Response, err error) bool {
return errors.As(err, &http2.GoAwayError{})
}
// nolint:unused
func httpRequestNGINXRetryCondition(resp *http.Response, _ error) bool {
return resp.StatusCode == http.StatusBadRequest &&
resp.Header.Get("Server") == "nginx" &&
resp.Header.Get("Content-Type") == "text/html"
}
// Helper function to extract APIError from response
// nolint:unused
func getAPIError(resp *http.Response) (*APIError, bool) {
var apiError APIError
err := json.NewDecoder(resp.Body).Decode(&apiError)
if err != nil {
return nil, false
}
return &apiError, true
}
|