Skip to content

Commit

Permalink
Propogate Do() HTTP error up when retries are exceeded
Browse files Browse the repository at this point in the history
Add propagation of the HTTP request/response error up the call stack when the
number of retries are exceeded. Because the ErrorHandler does not receive a copy
of the request, it's impossible to use the ErrorHandler to generate an enhanced
version of the default Retries Exceeded error message.

This change takes the non-API breaking approach to solving this, by including
the error string in the returned error (if an error has occurred).

Because the error string returned from `Do()` uses `%w`, this change requires Go
1.13+.

Fixes #69
  • Loading branch information
theckman committed Jun 9, 2020
1 parent 4af2e4b commit 8326782
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 14 deletions.
72 changes: 61 additions & 11 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,48 @@ func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bo
return false, nil
}

// ErrorPropagatedRetryPolicy is the same as DefaultRetryPolicy, except it
// propagates errors back instead of returning nil. This allows you to inspect
// why it decided to retry or not.
func ErrorPropagatedRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
// do not retry on context.Canceled or context.DeadlineExceeded
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 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 _, ok := v.Err.(x509.UnknownAuthorityError); ok {
return false, v
}
}

// The error is likely recoverable so retry.
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 != 501) {
return true, fmt.Errorf("unexpected HTTP status %s", resp.Status)
}

return false, nil
}

// DefaultBackoff provides a default callback for Client.Backoff which
// will perform exponential backoff based on the attempt number and limited
// by the provided minimum and maximum durations.
Expand Down Expand Up @@ -509,9 +551,12 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
}

var resp *http.Response
var attempt int
var err error

for i := 0; ; i++ {
attempt++

var code int // HTTP response code

// Always rewind the request body when non-nil.
Expand Down Expand Up @@ -572,12 +617,12 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
}

// Now decide if we should continue.
if checkErr != nil {
err = checkErr
}

if !checkOK {
if checkErr != nil {
err = checkErr
}
c.HTTPClient.CloseIdleConnections()
return resp, err
break
}

// We do this before drainBody because there's no need for the I/O if
Expand Down Expand Up @@ -613,19 +658,24 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
}
}

if err == nil {
return resp, nil
}

defer c.HTTPClient.CloseIdleConnections()

if c.ErrorHandler != nil {
c.HTTPClient.CloseIdleConnections()
return c.ErrorHandler(resp, err, c.RetryMax+1)
return c.ErrorHandler(resp, err, attempt)
}

// By default, we close the response body and return an error without
// returning the response
if resp != nil {
resp.Body.Close()
c.drainBody(resp.Body)
}
c.HTTPClient.CloseIdleConnections()
return nil, fmt.Errorf("%s %s giving up after %d attempts",
req.Method, req.URL, c.RetryMax+1)

return nil, fmt.Errorf("%s %s giving up after %d attempt(s): %w",
req.Method, req.URL, attempt, err)
}

// Try to read the response body so we can reuse this connection.
Expand Down
9 changes: 6 additions & 3 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
Expand Down Expand Up @@ -462,8 +463,10 @@ func TestClient_RequestWithContext(t *testing.T) {
t.Fatalf("CheckRetry called %d times, expected 1", called)
}

if err != context.Canceled {
t.Fatalf("Expected context.Canceled err, got: %v", err)
e := fmt.Sprintf("GET %s giving up after 1 attempt(s): %s", ts.URL, context.Canceled.Error())

if err.Error() != e {
t.Fatalf("Expected err to contain %s, got: %v", e, err)
}
}

Expand Down Expand Up @@ -493,7 +496,7 @@ func TestClient_CheckRetry(t *testing.T) {
t.Fatalf("CheckRetry called %d times, expected 1", called)
}

if err != retryErr {
if err.Error() != fmt.Sprintf("GET %s giving up after 2 attempt(s): retryError", ts.URL) {
t.Fatalf("Expected retryError, got:%v", err)
}

Expand Down

0 comments on commit 8326782

Please sign in to comment.