From bc22bdca070e5fdd16840bf440755384a59a426a Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:07:48 -0700 Subject: [PATCH] Fix infinite Spotify rate limit loop from unguarded auth probes and swallowed errors --- core/spotify_client.py | 20 +++++++++++--------- core/spotify_worker.py | 14 +++++++++----- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/core/spotify_client.py b/core/spotify_client.py index fbaf3ca1..f1ffc866 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -441,18 +441,20 @@ class SpotifyClient: error_str = str(e) # Rate limit means we ARE authenticated — just throttled if "rate" in error_str.lower() or "429" in error_str: - logger.warning("Spotify rate limited during auth check — treating as authenticated") - # Check if there's a Retry-After header indicating a long ban + # ANY rate limit on the auth probe means Spotify is actively throttling us. + # Always activate a global ban — even with a short or missing Retry-After. + # Without this, the probe→429→probe cycle repeats every ~60s forever. retry_after = None if hasattr(e, 'headers') and e.headers: retry_after = e.headers.get('Retry-After') or e.headers.get('retry-after') - if retry_after: - try: - delay = int(retry_after) - if delay > _LONG_RATE_LIMIT_THRESHOLD: - _set_global_rate_limit(delay, 'is_spotify_authenticated') - except (ValueError, TypeError): - pass + try: + delay = int(retry_after) if retry_after else 0 + except (ValueError, TypeError): + delay = 0 + # Minimum 10 minutes for auth probe 429s — these indicate persistent throttling + ban_duration = max(delay, 600) + _set_global_rate_limit(ban_duration, 'is_spotify_authenticated') + logger.warning(f"Auth probe rate limited — activating {ban_duration}s global ban") result = True else: logger.debug(f"Spotify authentication check failed: {e}") diff --git a/core/spotify_worker.py b/core/spotify_worker.py index 9b7be78e..dfbd97c6 100644 --- a/core/spotify_worker.py +++ b/core/spotify_worker.py @@ -149,12 +149,14 @@ class SpotifyWorker: time.sleep(min(cooldown, 60)) continue - # Auth guard — don't process anything without Spotify auth - if not self.client.is_spotify_authenticated(): - # Try reloading config in case user re-authenticated via settings + # Auth guard — check if Spotify client is configured (no API call). + # We intentionally avoid calling is_spotify_authenticated() here + # because it makes an API probe that can re-trigger rate limits + # and lock users in an infinite rate-limit loop. + if not self.client.sp: self.client.reload_config() - if not self.client.is_spotify_authenticated(): - logger.debug("Spotify not authenticated, sleeping 30s...") + if not self.client.sp: + logger.debug("Spotify not configured, sleeping 30s...") time.sleep(30) continue @@ -343,6 +345,8 @@ class SpotifyWorker: elif item_type == 'track_individual': self._process_track_individual(item) + except SpotifyRateLimitError: + raise # Propagate to main loop so it activates the sleep/ban guard except Exception as e: logger.error(f"Error processing {item.get('type')} '{item.get('name', '')}': {e}") self.stats['errors'] += 1