I am trying to verify if the go-retryablehttp execution performs retries as per the specified config.
The verification methodology is to create a test that
The above is what I have tried to capture in the below code block
//function that returns 500 error
func InternalServerErrorHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, fmt.Sprintf("test_%d_body", http.StatusInternalServerError), http.StatusInternalServerError)
}
func TestCreateToolsClient(t *testing.T) {
//create a new server
ts := httptest.NewServer(http.HandlerFunc(InternalServerErrorHandler))
defer ts.Close()
//create a request
request, err := retryablehttp.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
log.Fatal(err)
}
//create a retryable client
var options retryablehttp.Options
options.RetryWaitMin = 10 * time.Millisecond
options.RetryWaitMax = 50 * time.Millisecond
options.RetryMax = 6
options.Timeout = 60000 * time.Millisecond
retryableClient := retryablehttp.NewClient(options)
retryCount := -1
// to verify from stdout if the # of retry actually is getting counted
retryableClient.RequestLogHook = func(req *http.Request, retryNumber int) {
retryCount = retryNumber
log.Println("Retrying")
}
//execute the request
response, err := retryableClient.Do(request)
if err != nil {
return
}
//verify
require.Equal(t, http.StatusInternalServerError, response.StatusCode)
require.Equal(t, 2, retryCount)
}
My understanding is
retryableClient.Do(request)
should take time=Timeout if there is erroroptions.RetryMax = 6
timesI tried debugging the code, and turns out
// Attempt the request
resp, err = c.HTTPClient.Do(req.Request)
here has err as nil
.
Unsure what am I doing wrong.
I have created a go playground here
Okay, I figured this out.
Go playground with solution here
My version of go is Go 1.17. If you run the above code in go playground (which has Go version 1.19), the retry works.
For Go 1.17, retryable-http(v1.0.2) does not handle status error codes
func DefaultRetryPolicy() func(ctx context.Context, resp *http.Response, err error) (bool, error) {
return func(ctx context.Context, resp *http.Response, err error) (bool, error) {
// do not retry on context.Canceled or context.DeadlineExceeded
//fmt.Printf("jkajsuiohsd %v\n", ctx.Err())
if ctx.Err() != nil {
return false, ctx.Err()
}
if err != nil {
if v, ok := err.(*url.Error); ok {
// Don't retry if the error was due to too many redirects.
if redirectsErrorRegex.MatchString(v.Error()) {
return false, nil
}
// Don't retry if the error was due to an invalid protocol scheme.
if schemeErrorRegex.MatchString(v.Error()) {
return false, nil
}
// Don't retry if the error was due to TLS cert verification failure.
if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
return false, nil
}
}
// The error is likely recoverable so retry.
return true, nil
}
//EXPECT HANDLING BASED ON STATUS CODES, BUT ABSENT
return false, nil
}
}
For Go 1.19, retryable-http(v2.1) implements the functionality under baseRetryPolicy
as shown here
func baseRetryPolicy(resp *http.Response, err error) (bool, error) {
if err != nil {
if v, ok := err.(*url.Error); ok {
// Don't retry if the error was due to too many redirects.
if redirectsErrorRe.MatchString(v.Error()) {
return false, v
}
// Don't retry if the error was due to an invalid protocol scheme.
if schemeErrorRe.MatchString(v.Error()) {
return false, v
}
// Don't retry if the error was due to TLS cert verification failure.
if notTrustedErrorRe.MatchString(v.Error()) {
return false, v
}
if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
return false, v
}
}
// The error is likely recoverable so retry.
return true, nil
}
// 429 Too Many Requests is recoverable. Sometimes the server puts
// a Retry-After response header to indicate when the server is
// available to start processing request from client.
if resp.StatusCode == http.StatusTooManyRequests {
return true, nil
}
// Check the response code. We retry on 500-range responses to allow
// the server time to recover, as 500's are typically not permanent
// errors and may relate to outages on the server side. This will catch
// invalid response codes as well, like 0 and 999.
//THIS PART HERE FLAGS RETRIES ON STATUS CODES!
if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) {
return true, fmt.Errorf("unexpected HTTP status %s", resp.Status)
}
return false, nil
}
Finally, one has the following options.
func TestCreateToolsClient(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(InternalServerErrorHandler))
defer ts.Close()
request, err := retryablehttp.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
log.Fatal(err)
}
var options retryablehttp.Options
options.RetryWaitMin = 10 * time.Millisecond
options.RetryWaitMax = 50 * time.Millisecond
options.RetryMax = 6
options.Timeout = 60 * time.Second
//options.Timeout = 30000 * time.Millisecond
retryableClient := retryablehttp.NewClient(options)
retryCount := -1
// to verify from stdout if the # of retry actually is getting counted
retryableClient.RequestLogHook = func(req *http.Request, retryNumber int) {
retryCount = retryNumber
log.Println("Retrying")
}
// A regular expression to match the error returned by net/http when the
// configured number of redirects is exhausted. This error isn't typed
// specifically so we resort to matching on the error string.
redirectsErrorRe := regexp.MustCompile(`stopped after \d+ redirects\z`)
// A regular expression to match the error returned by net/http when the
// scheme specified in the URL is invalid. This error isn't typed
// specifically so we resort to matching on the error string.
schemeErrorRe := regexp.MustCompile(`unsupported protocol scheme`)
// A regular expression to match the error returned by net/http when the
// TLS certificate is not trusted. This error isn't typed
// specifically so we resort to matching on the error string.
notTrustedErrorRe := regexp.MustCompile(`certificate is not trusted`)
retryableClient.CheckRetry = func(_ context.Context, resp *http.Response, err error) (bool, error) {
if err != nil {
if v, ok := err.(*url.Error); ok {
// Don't retry if the error was due to too many redirects.
if redirectsErrorRe.MatchString(v.Error()) {
return false, v
}
// Don't retry if the error was due to an invalid protocol scheme.
if schemeErrorRe.MatchString(v.Error()) {
return false, v
}
// Don't retry if the error was due to TLS cert verification failure.
if notTrustedErrorRe.MatchString(v.Error()) {
return false, v
}
if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
return false, v
}
}
// The error is likely recoverable so retry.
return true, nil
}
// 429 Too Many Requests is recoverable. Sometimes the server puts
// a Retry-After response header to indicate when the server is
// available to start processing request from client.
if resp.StatusCode == http.StatusTooManyRequests {
return true, nil
}
// Check the response code. We retry on 500-range responses to allow
// the server time to recover, as 500's are typically not permanent
// errors and may relate to outages on the server side. This will catch
// invalid response codes as well, like 0 and 999.
if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) {
return true, fmt.Errorf("unexpected HTTP status %s", resp.Status)
}
return false, nil
}