Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions spotify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
}
Expand Down
29 changes: 28 additions & 1 deletion spotify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down