From 69a4683cf479df73eedba92969f29bc2e090e810 Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Fri, 15 Nov 2024 12:45:17 -0600 Subject: [PATCH] Expose RetryAfter value in error --- spotify.go | 6 ++++++ spotify_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/spotify.go b/spotify.go index 54a4f51..ca56170 100644 --- a/spotify.go +++ b/spotify.go @@ -152,6 +152,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 { @@ -189,6 +192,9 @@ func (c *Client) 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 9eb11e0..b8416f5 100644 --- a/spotify_test.go +++ b/spotify_test.go @@ -2,14 +2,17 @@ package spotify import ( "context" - "golang.org/x/oauth2" + "errors" "io" "net/http" "net/http/httptest" "os" + "strconv" "strings" "testing" "time" + + "golang.org/x/oauth2" ) func testClient(code int, body io.Reader, validators ...func(*http.Request)) (*Client, *httptest.Server) { @@ -104,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{ @@ -144,14 +193,14 @@ func TestClient_Token(t *testing.T) { t.Run("non oauth2 transport", func(t *testing.T) { client := &Client{ - http: http.DefaultClient, + http: http.DefaultClient, } _, err := client.Token() if err == nil || err.Error() != "spotify: client not backed by oauth2 transport" { t.Errorf("Should throw error: %s", "spotify: client not backed by oauth2 transport") } }) - + t.Run("invalid token", func(t *testing.T) { httpClient := config.Client(context.Background(), nil) client := New(httpClient)