diff --git a/spotify.go b/spotify.go index c04e5c2..c4c894c 100644 --- a/spotify.go +++ b/spotify.go @@ -148,6 +148,9 @@ type Error struct { Message string `json:"message"` // The HTTP status code. Status int `json:"status"` + // RetryAfter contains the time before which client should not retry a + // rate-limited request, calculated from the Retry-After header, when present. + RetryAfter time.Time `json:"-"` } func (e Error) Error() string { @@ -196,6 +199,9 @@ func decodeError(resp *http.Response) error { e.E.Message = fmt.Sprintf("spotify: unexpected HTTP %d: %s (empty error)", resp.StatusCode, http.StatusText(resp.StatusCode)) } + if retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After")); retryAfter != 0 { + e.E.RetryAfter = time.Now().Add(time.Duration(retryAfter) * time.Second) + } return e.E } diff --git a/spotify_test.go b/spotify_test.go index 635e2ac..91c1c00 100644 --- a/spotify_test.go +++ b/spotify_test.go @@ -2,10 +2,12 @@ package spotify import ( "context" + "errors" "io" "net/http" "net/http/httptest" "os" + "strconv" "strings" "testing" "time" @@ -105,6 +107,52 @@ func TestNewReleasesRateLimitExceeded(t *testing.T) { } } +func TestRateLimitExceededReportsRetryAfter(t *testing.T) { + t.Parallel() + const retryAfter = 2 + + handlers := []http.HandlerFunc{ + // first attempt fails + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Retry-After", strconv.Itoa(retryAfter)) + w.WriteHeader(rateLimitExceededStatusCode) + _, _ = io.WriteString(w, `{ "error": { "message": "slow down", "status": 429 } }`) + }), + // next attempt succeeds + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f, err := os.Open("test_data/new_releases.txt") + if err != nil { + t.Fatal(err) + } + defer f.Close() + _, err = io.Copy(w, f) + if err != nil { + t.Fatal(err) + } + }), + } + + i := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handlers[i](w, r) + i++ + })) + defer server.Close() + + client := &Client{http: http.DefaultClient, baseURL: server.URL + "/"} + _, err := client.NewReleases(context.Background()) + if err == nil { + t.Fatal("expected an error") + } + 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{