diff --git a/spotify.go b/spotify.go index c4c894c..11930cf 100644 --- a/spotify.go +++ b/spotify.go @@ -38,8 +38,9 @@ type Client struct { http *http.Client baseURL string - autoRetry bool - acceptLanguage string + autoRetry bool + acceptLanguage string + maxRetryDuration time.Duration } type ClientOption func(client *Client) @@ -66,6 +67,15 @@ func WithAcceptLanguage(lang string) ClientOption { } } +// WithMaxRetryDuration limits the amount of time that the client will wait to retry after being rate limited. +// If the retry time is longer than the max, then the client will return an error. +// This option only works when auto retry is enabled +func WithMaxRetryDuration(duration time.Duration) ClientOption { + return func(client *Client) { + client.maxRetryDuration = duration + } +} + // New returns a client for working with the Spotify Web API. // The provided httpClient must provide Authentication with the requests. // The auth package may be used to generate a suitable client. @@ -239,10 +249,14 @@ func (c *Client) execute(req *http.Request, result interface{}, needsStatus ...i if c.autoRetry && isFailure(resp.StatusCode, needsStatus) && shouldRetry(resp.StatusCode) { + duration := retryDuration(resp) + if c.maxRetryDuration > 0 && duration > c.maxRetryDuration { + return decodeError(resp) + } select { case <-req.Context().Done(): // If the context is cancelled, return the original error - case <-time.After(retryDuration(resp)): + case <-time.After(duration): continue } } @@ -294,10 +308,14 @@ func (c *Client) get(ctx context.Context, url string, result interface{}) error defer resp.Body.Close() if resp.StatusCode == http.StatusTooManyRequests && c.autoRetry { + duration := retryDuration(resp) + if c.maxRetryDuration > 0 && duration > c.maxRetryDuration { + return decodeError(resp) + } select { case <-ctx.Done(): // If the context is cancelled, return the original error - case <-time.After(retryDuration(resp)): + case <-time.After(duration): continue } } diff --git a/spotify_test.go b/spotify_test.go index 91c1c00..8747d42 100644 --- a/spotify_test.go +++ b/spotify_test.go @@ -115,7 +115,7 @@ func TestRateLimitExceededReportsRetryAfter(t *testing.T) { // first attempt fails http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Retry-After", strconv.Itoa(retryAfter)) - w.WriteHeader(rateLimitExceededStatusCode) + w.WriteHeader(http.StatusTooManyRequests) _, _ = io.WriteString(w, `{ "error": { "message": "slow down", "status": 429 } }`) }), // next attempt succeeds @@ -153,6 +153,33 @@ func TestRateLimitExceededReportsRetryAfter(t *testing.T) { } } +func TestRateLimitExceededMaxRetryConfig(t *testing.T) { + t.Parallel() + const retryAfter = 3660 // 61 minutes + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Retry-After", strconv.Itoa(retryAfter)) + w.WriteHeader(http.StatusTooManyRequests) + _, _ = io.WriteString(w, `{ "error": { "message": "slow down", "status": 429 } }`) + }) + server := httptest.NewServer(handler) + defer server.Close() + client := &Client{ + http: http.DefaultClient, + baseURL: server.URL + "/", + autoRetry: true, + maxRetryDuration: time.Hour, + } + + _, err := client.NewReleases(context.Background()) + var spotifyError Error + if !errors.As(err, &spotifyError) { + t.Fatalf("expected a spotify error, got %T", err) + } + if retryAfter*time.Second-time.Until(spotifyError.RetryAfter) > time.Second { + t.Error("expected RetryAfter value") + } +} + func TestClient_Token(t *testing.T) { // oauth setup for valid test token config := oauth2.Config{