Skip to content

Commit

Permalink
Added support for HTTP 429 Retry-After as per Issue #99 (#100)
Browse files Browse the repository at this point in the history
* added support for HTTP 429 Retry-After.

This was added in DefaultPolicy and DefaultBackoff. The latter will
examine the Retry-After response header (if existant) and do a backoff
for the specified amount of time. Otherwise it will default to default
backoff behaviour.
  • Loading branch information
mariotoffia committed Jun 22, 2020
1 parent cf855b1 commit 1831df7
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 0 deletions.
22 changes: 22 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"net/url"
"os"
"regexp"
"strconv"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -425,6 +426,13 @@ func DefaultRetryPolicy(ctx context.Context, resp *http.Response, err error) (bo
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
Expand Down Expand Up @@ -481,7 +489,21 @@ func ErrorPropagatedRetryPolicy(ctx context.Context, resp *http.Response, err er
// 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.
//
// It also tries to parse Retry-After response header when a http.StatusTooManyRequests
// (HTTP Code 429) is found in the resp parameter. Hence it will return the number of
// seconds the server states it may be ready to process more requests from this client.
func DefaultBackoff(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
if resp != nil {
if resp.StatusCode == http.StatusTooManyRequests {
if s, ok := resp.Header["Retry-After"]; ok {
if sleep, err := strconv.ParseInt(s[0], 10, 64); err == nil {
return time.Second * time.Duration(sleep)
}
}
}
}

mult := math.Pow(2, float64(attemptNum)) * float64(min)
sleep := time.Duration(mult)
if float64(sleep) != mult || sleep > max {
Expand Down
32 changes: 32 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,38 @@ func TestClient_CheckRetry(t *testing.T) {
}
}

func TestClient_DefaultBackoff429TooManyRequest(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Retry-After", "2")
http.Error(w, "test_429_body", http.StatusTooManyRequests)
}))
defer ts.Close()

client := NewClient()

var retryAfter time.Duration
retryable := false

client.CheckRetry = func(_ context.Context, resp *http.Response, err error) (bool, error) {
retryable, _ = DefaultRetryPolicy(context.Background(), resp, err)
retryAfter = DefaultBackoff(client.RetryWaitMin, client.RetryWaitMax, 1, resp)
return false, nil
}

_, err := client.Get(ts.URL)
if err != nil {
t.Fatalf("expected no errors since retryable")
}

if !retryable {
t.Fatal("Since 429 is recoverable, the default policy shall return true")
}

if retryAfter != 2*time.Second {
t.Fatalf("The header Retry-After specified 2 seconds, and shall not be %d seconds", retryAfter/time.Second)
}
}

func TestClient_DefaultRetryPolicy_TLS(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
Expand Down

0 comments on commit 1831df7

Please sign in to comment.