Skip to content

feat(api): retry Spotify 429s with backoff - #413

Merged
chiptus merged 14 commits into
mainfrom
fix-411/spotify-429-retry
Aug 31, 2026
Merged

feat(api): retry Spotify 429s with backoff#413
chiptus merged 14 commits into
mainfrom
fix-411/spotify-429-retry

Conversation

@chiptus

@chiptus chiptus commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Adds retry-with-backoff for Spotify API rate limits, honoring Retry-After header and surfacing rate-limit errors to the UI with wait time.
All tests passing; integration test for end-to-end rate-limit surface path planned in follow-up.

Verification

  • Edge function search-artist-links: call with artist name and trigger Spotify 429; verify retries and either succeeds or returns rate-limit error with wait time
  • Link Wizard UI: when rate-limit error surfaces, verify "Rate limited. Try again in Ns" message displays instead of generic error
  • Edge function with 5xx/4xx (non-429) error: verify fails immediately without retry
  • Spotify token refresh: verify also retries on 429 before failing

Generated by Claude Code

Copilot AI lite review requested due to automatic review settings August 26, 2026 16:18
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
upline Ready Ready Preview Aug 31, 2026 5:41am

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The retry utility currently parses Retry-After but doesn’t use it to pace 429 retries, contradicting the stated behavior and risking immediate repeated rate-limit hits.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a shared retry-with-backoff utility for Supabase edge functions and uses it to handle Spotify 429 rate limits, propagating Retry-After wait time through the API response so the Link Wizard UI can display a rate-limit-specific message.

Changes:

  • Introduces fetchWithRetry (+ unit tests) for retrying 429s with backoff and returning a structured rate-limit result.
  • Updates Spotify search and Spotify token acquisition to use the retry utility and surface rate-limit wait time.
  • Plumbs rateLimitRetryAfter through edge-function types, API Zod schema, and Link Wizard error formatting.
File summaries
File Description
supabase/functions/search-artist-links/types.ts Adds optional rateLimitRetryAfter to provider outcomes/results.
supabase/functions/search-artist-links/spotify-adapter.ts Switches Spotify search to fetchWithRetry and returns rate-limit info on 429.
supabase/functions/search-artist-links/index.ts Includes rateLimitRetryAfter in the edge-function response when present.
supabase/functions/_shared/spotify-api/auth.ts Uses fetchWithRetry for token requests and surfaces rate-limit failures.
supabase/functions/_shared/retry-utils.ts Adds new retry/backoff helper and Retry-After parsing.
supabase/functions/_shared/retry-utils.test.ts Adds Deno unit tests covering retry behavior and Retry-After parsing.
src/pages/admin/festivals/LinkWizard/useProviderCandidates.ts Threads through rateLimitRetryAfter and formats a user-facing rate-limit message.
src/api/artistSearch/types.ts Extends client schema/types to accept rateLimitRetryAfter.
Review details

Suppressed comments (1)

supabase/functions/_shared/retry-utils.ts:50

  • On 429, the retry sleep uses only exponential backoff; it should consider the parsed Retry-After value so we don’t retry earlier than the server indicates (subject to maxDelayMs).
          if (attempt < maxRetries) {
            const delay = Math.min(
              initialDelayMs * Math.pow(2, attempt),
              maxDelayMs,
            );
  • Files reviewed: 9/9 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/pages/admin/festivals/LinkWizard/useProviderCandidates.ts Outdated
Comment thread supabase/functions/_shared/retry-utils.ts
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

Deploy → stagingworkflow run
Last updated: 2026-08-31 05:41:13 UTC

  • ⏭️ DB migrations skipped (no changes)
  • Edge functions succeeded

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

Playwright test results

passed  55 passed

Details

stats  55 tests across 16 suites
duration  1 minute, 15 seconds
commit  6ac01f5

claude added 5 commits August 27, 2026 17:59
Implement retry-with-backoff helper for handling Spotify API 429 responses:
- New fetchWithRetry utility honors Retry-After header for rate limits
- Spotify search and token refresh now retry twice with exponential backoff
- Rate-limit errors are distinguishable from other failures with wait time
- UI displays "Rate limited. Try again in Ns" message when applicable
- Includes comprehensive unit tests for retry scenarios

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2ci2Q1QgHtkMY93c2zf9C
Adds integration test covering repeated 429 responses surfacing rate-limit error through full request path. Tests three scenarios:
- 429 exhausting retries: returns distinguishable rate-limit error with Retry-After seconds
- 429 then success: recovers after one retry with backoff
- Non-429 error: fails immediately without retry

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2ci2Q1QgHtkMY93c2zf9C
…e UI from error wording

Two fixes:
1. fetchWithRetry now incorporates Retry-After header into sleep duration using max(exponentialBackoff, retryAfterMs), capped at maxDelayMs, so we honor the server's requested wait time
2. buildErrorMessage detects rate-limit via rateLimitRetryAfter field directly instead of string matching on error text, decoupling UI from backend error wording

Adds unit tests verifying:
- Retry-After value is respected and delay is at least as long as requested
- Large Retry-After values are capped at maxDelayMs to prevent hanging

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2ci2Q1QgHtkMY93c2zf9C
…use fake timers in retry tests

- getSpotifyAccessToken no longer reads .error off a RateLimitError result (TS2339)
- searchSpotify breaks out of the artist loop on a 429 instead of hammering remaining candidates with requests that will just be rate-limited again
- retry-utils.test.ts uses Deno's FakeTime instead of waiting on real delays, cutting ~3s of real time off the suite
…integration tests

getSpotifyAccessToken threw "Spotify credentials are not configured" in CI
since SPOTIFY_CLIENT_ID/SECRET aren't set for deno test. Also the module-level
token cache persisted across the file's three Deno.test cases (they share the
same imported auth.ts instance), so tests after the first skipped the mocked
token fetch and desynced their response queues.
…lpers

Extracts getCachedToken() and requestSpotifyToken() to make the
credential-check/cache/fetch flow easier to follow.
Separates the HTTP/retry/error-handling concern (fetchTokenResponse)
from response validation (parseTokenResponse) for clarity.
Pulls the rate-limit/other-error branching out of fetchTokenResponse
into its own function.
Converts the 429-exhausted and maxDelayMs-cap tests to advance a
FakeTime clock instead of waiting on real setTimeout delays, matching
the pattern already used by the other retry-utils tests. Removes
reliance on wall-clock elapsed-time assertions, which were flaky under
load.
…apter

Adds fetchSpotifyAPI to _shared/spotify-api/api.ts, mirroring the
fetchSoundCloudAPI pattern: retry + schema validation in one place.
getSpotifyArtistById now goes through it too, gaining 429 retry
handling it previously lacked. spotify-adapter.ts's searchSpotify is
split into searchSpotifyArtist, handleSearchFailure, and
fillRemainingWithRateLimit for readability.

Also adds an optional status field to retry-utils' OtherError so
callers can branch on HTTP status (e.g. 404) without inspecting error
message text.
@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

Playwright test results

passed  353 passed

Details

stats  353 tests across 25 suites
duration  4 minutes, 16 seconds
commit  863d4c3

…test

FakeTime.tickAsync jumps straight to the target time after a single
microtask flush, so a single big tick only fires the first of two
sequential retry sleeps — the second sleep is scheduled only once
the fake clock has already jumped past it, so it never fires and the
test hangs. That leaves the FakeTime instance's using-disposal stuck
mid-await, leaking faked globals into every later test in the file
(the "Cannot construct FakeTime: time is already faked" failures).
Ticking once per expected sleep fixes all five cascading failures.
@chiptus
chiptus merged commit 0293c55 into main Aug 31, 2026
19 checks passed
@chiptus
chiptus deleted the fix-411/spotify-429-retry branch August 31, 2026 06:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Retry Spotify 429s with backoff before surfacing a rate-limit message

3 participants