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 occured).

Fixes #69
  • Loading branch information
theckman committed Oct 15, 2019
1 parent 46f7ba5 commit 611ca1b
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 15 deletions.
34 changes: 22 additions & 12 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bo
// 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, nil
return true, fmt.Errorf("unexpected HTTP status %s", resp.Status)
}

return false, nil
Expand Down Expand Up @@ -438,9 +438,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 @@ -501,12 +504,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 beause there's no need for the I/O if
Expand Down Expand Up @@ -542,19 +545,26 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
}
}

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

if err == nil {
return resp, nil
}

if c.ErrorHandler != nil {
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): %v",
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 611ca1b

Please sign in to comment.