retries_http.go raw
1 package linodego
2
3 import (
4 "encoding/json"
5 "errors"
6 "log"
7 "net/http"
8 "strconv"
9 "time"
10
11 "golang.org/x/net/http2"
12 )
13
14 const (
15 // nolint:unused
16 httpRetryAfterHeaderName = "Retry-After"
17 // nolint:unused
18 httpMaintenanceModeHeaderName = "X-Maintenance-Mode"
19
20 // nolint:unused
21 httpDefaultRetryCount = 1000
22 )
23
24 // RetryConditional is a type alias for a function that determines if a request should be retried based on the response and error.
25 // nolint:unused
26 type httpRetryConditional func(*http.Response, error) bool
27
28 // RetryAfter is a type alias for a function that determines the duration to wait before retrying based on the response.
29 // nolint:unused
30 type httpRetryAfter func(*http.Response) (time.Duration, error)
31
32 // Configures http.Client to lock until enough time has passed to retry the request as determined by the Retry-After response header.
33 // If the Retry-After header is not set, we fall back to the value of SetPollDelay.
34 // nolint:unused
35 func httpConfigureRetries(c *httpClient) {
36 c.retryConditionals = append(c.retryConditionals, httpcheckRetryConditionals(c))
37 c.retryAfter = httpRespectRetryAfter
38 }
39
40 // nolint:unused
41 func httpcheckRetryConditionals(c *httpClient) httpRetryConditional {
42 return func(resp *http.Response, err error) bool {
43 for _, retryConditional := range c.retryConditionals {
44 retry := retryConditional(resp, err)
45 if retry {
46 log.Printf("[INFO] Received error %v - Retrying", err)
47 return true
48 }
49 }
50
51 return false
52 }
53 }
54
55 // nolint:unused
56 func httpRespectRetryAfter(resp *http.Response) (time.Duration, error) {
57 retryAfterStr := resp.Header.Get(retryAfterHeaderName)
58 if retryAfterStr == "" {
59 return 0, nil
60 }
61
62 retryAfter, err := strconv.Atoi(retryAfterStr)
63 if err != nil {
64 return 0, err
65 }
66
67 duration := time.Duration(retryAfter) * time.Second
68 log.Printf("[INFO] Respecting Retry-After Header of %d (%s)", retryAfter, duration)
69
70 return duration, nil
71 }
72
73 // Retry conditions
74
75 // nolint:unused
76 func httpLinodeBusyRetryCondition(resp *http.Response, _ error) bool {
77 apiError, ok := getAPIError(resp)
78 linodeBusy := ok && apiError.Error() == "Linode busy."
79 retry := resp.StatusCode == http.StatusBadRequest && linodeBusy
80
81 return retry
82 }
83
84 // nolint:unused
85 func httpTooManyRequestsRetryCondition(resp *http.Response, _ error) bool {
86 return resp.StatusCode == http.StatusTooManyRequests
87 }
88
89 // nolint:unused
90 func httpServiceUnavailableRetryCondition(resp *http.Response, _ error) bool {
91 serviceUnavailable := resp.StatusCode == http.StatusServiceUnavailable
92
93 // During maintenance events, the API will return a 503 and add
94 // an `X-MAINTENANCE-MODE` header. Don't retry during maintenance
95 // events, only for legitimate 503s.
96 if serviceUnavailable && resp.Header.Get(maintenanceModeHeaderName) != "" {
97 log.Printf("[INFO] Linode API is under maintenance, request will not be retried - please see status.linode.com for more information")
98 return false
99 }
100
101 return serviceUnavailable
102 }
103
104 // nolint:unused
105 func httpRequestTimeoutRetryCondition(resp *http.Response, _ error) bool {
106 return resp.StatusCode == http.StatusRequestTimeout
107 }
108
109 // nolint:unused
110 func httpRequestGOAWAYRetryCondition(_ *http.Response, err error) bool {
111 return errors.As(err, &http2.GoAwayError{})
112 }
113
114 // nolint:unused
115 func httpRequestNGINXRetryCondition(resp *http.Response, _ error) bool {
116 return resp.StatusCode == http.StatusBadRequest &&
117 resp.Header.Get("Server") == "nginx" &&
118 resp.Header.Get("Content-Type") == "text/html"
119 }
120
121 // Helper function to extract APIError from response
122 // nolint:unused
123 func getAPIError(resp *http.Response) (*APIError, bool) {
124 var apiError APIError
125
126 err := json.NewDecoder(resp.Body).Decode(&apiError)
127 if err != nil {
128 return nil, false
129 }
130
131 return &apiError, true
132 }
133