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
|
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
}
|