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