github.com/newrelic/newrelic-client-go@v1.1.0/internal/http/retry.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/x509"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"regexp"
    11  
    12  	"github.com/valyala/fastjson"
    13  )
    14  
    15  var (
    16  	// A regular expression to match the error returned by net/http when the
    17  	// configured number of redirects is exhausted. This error isn't typed
    18  	// specifically so we resort to matching on the error string.
    19  	redirectsErrorRe = regexp.MustCompile(`stopped after \d+ redirects\z`)
    20  
    21  	// A regular expression to match the error returned by net/http when the
    22  	// scheme specified in the URL is invalid. This error isn't typed
    23  	// specifically so we resort to matching on the error string.
    24  	schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`)
    25  )
    26  
    27  // RetryPolicy provides a callback for retryablehttp's CheckRetry, which
    28  // will retry on connection errors and server errors.
    29  func RetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
    30  	// do not retry on context.Canceled or context.DeadlineExceeded
    31  	if ctx.Err() != nil {
    32  		return false, ctx.Err()
    33  	}
    34  
    35  	if err != nil {
    36  		if v, ok := err.(*url.Error); ok {
    37  			// Don't retry if the error was due to too many redirects.
    38  			if redirectsErrorRe.MatchString(v.Error()) {
    39  				return false, nil
    40  			}
    41  
    42  			// Don't retry if the error was due to an invalid protocol scheme.
    43  			if schemeErrorRe.MatchString(v.Error()) {
    44  				return false, nil
    45  			}
    46  
    47  			// Don't retry if the error was due to TLS cert verification failure.
    48  			if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
    49  				return false, nil
    50  			}
    51  		}
    52  
    53  		// The error is likely recoverable so retry.
    54  		return true, nil
    55  	}
    56  
    57  	// Check the response code. We retry on 5xx responses to allow
    58  	// the server time to recover, as 5xx's are typically not permanent
    59  	// errors and may relate to outages on the server side. We are notably
    60  	// disallowing retries for 500 errors here as the underlying APIs use them to
    61  	// provide useful error validation messages that should be passed back to the
    62  	// end user. This will catch invalid response codes as well, like 0 and 999.
    63  	// 429 Too Many Requests is retried as well to handle the aggressive rate limiting
    64  	// of the Synthetics API.
    65  	if resp.StatusCode == 0 ||
    66  		resp.StatusCode == 429 ||
    67  		resp.StatusCode == 500 ||
    68  		resp.StatusCode >= 502 {
    69  		return true, nil
    70  	}
    71  
    72  	// Check for json response and description for retries.
    73  	// i.e.: when receiving TOO_MANY_REQUESTS it is a HTTP 200 code with an
    74  	// error status that needed to be retried.
    75  	json, jErr := io.ReadAll(resp.Body)
    76  	resp.Body = io.NopCloser(bytes.NewBuffer(json))
    77  	if jErr == nil {
    78  		errorClass := fastjson.GetString(json, "errors", "0", "extensions", "errorClass")
    79  		if errorClass == ErrClassTooManyRequests.Error() {
    80  			return true, ErrClassTooManyRequests
    81  		}
    82  	}
    83  
    84  	return false, nil
    85  }