Skip to content

Fall back to per-track reads when batching is withheld - #22

Open
jonico wants to merge 1 commit into
jamiew:mainfrom
jonico:fix/track-info-batch-fallback
Open

Fall back to per-track reads when batching is withheld#22
jonico wants to merge 1 commit into
jamiew:mainfrom
jonico:fix/track-info-batch-fallback

Conversation

@jonico

@jonico jonico commented Aug 25, 2026

Copy link
Copy Markdown

Problem

get_track_info fails for two or more IDs on a restricted app.

Anything longer than one ID goes through /v1/tracks?ids=, which is withheld — while the single-track route works fine for those exact same IDs:

GET /v1/tracks/?ids=6ADPS6h7kuqZwqekUrLTEp,7AvWmlcIj7LhwnVqNT4UYa -> 403 Forbidden
GET /v1/tracks/6ADPS6h7kuqZwqekUrLTEp                             -> 200 OK
GET /v1/tracks/7AvWmlcIj7LhwnVqNT4UYa                             -> 200 OK

The failure boundary sits exactly on the len(ids) == 1 branch, which makes it easy to miss:

call result
get_track_info("6ADPS...") works
get_track_info(["6ADPS..."]) works
get_track_info(["6ADPS...", "7AvWm..."]) playback_restricted

So the batching the docstring advertises — "50 tracks = 1 API call instead of 50" — isn't merely slower than promised, it's unavailable, and simply asking for two tracks errors out. I found this trying to put two track IDs in one request and assuming I'd hit a rate limit or a bad ID.

Fix

with_fallback doesn't fit here: there's no alternative batch path to swap in, only a different number of requests. So try the batch, and on 401/403 degrade to one request per track.

  • The decision is remembered for the life of the process, so the 403 is paid once rather than on every call.
  • A single ID keeps using the single-track route, so it never provokes the 403 in the first place.
  • Any other status (e.g. 500) still raises rather than silently fanning out into N requests.
  • Null entries are dropped from a batch result, matching the previous behaviour.

Callers see no difference beyond latency, which is why this belongs in spotify_api rather than in each caller. The docstring now says batching may fall back instead of promising something the app may not be able to do.

Testing

  • ruff check, ruff format --check, mypy src/, bandit -r src/ all clean
  • pytest: 177 passed (8 new — single-ID never batches, batch when allowed, null entries dropped, 401 and 403 both degrade, the decision is cached, other statuses propagate, URIs accepted)
  • Live-verified on a restricted app, which is where the offline suite is blind:
withheld before: False
3 ids -> 3 tracks:
   Und dann kommt ein Mensch — Major Sunburn
   Was Ist Freundschaft Wert (Major Sunburn Harmony Edit) — By York
   Was ist die Freundschaft wert — MC Crome
withheld after: True
2 ids again -> 2 tracks (no second 403)
single string -> 1 track: Und dann kommt ein Mensch

The 3-ID call previously failed outright.

Relationship to my other PRs

Independent of #19, #20 and #21 — no shared lines, any merge order works. It's the same class of defect as #19 (get_artist_info): a withheld endpoint inside a tool taking the whole tool down rather than degrading. If you'd prefer these consolidated into one "degrade instead of failing on withheld endpoints" PR, happy to do that.

Note

No CHANGELOG.md entry deliberately — sibling fixes would all conflict on the same lines. Happy to add one to whichever lands first.

get_track_info fails for two or more IDs on a restricted app. It routes anything
longer than one ID through /v1/tracks?ids=, which answers 403 Forbidden, while
/v1/tracks/{id} still works for each of those same IDs. So the batching the
docstring advertises -- "50 tracks = 1 API call instead of 50" -- is not merely
slower than promised, it is unavailable, and asking for two tracks errors out.

The boundary is exactly at the len(ids) == 1 branch:

    get_track_info("6ADPS...")              -> ok
    get_track_info(["6ADPS..."])            -> ok
    get_track_info(["6ADPS...", "7AvWm..."]) -> playback_restricted

with_fallback does not fit: there is no alternative batch path to swap in, only a
different number of requests. So try the batch, and on a 401/403 degrade to one
request per track and remember that for the life of the process, so the 403 is
paid once rather than on every call. A single ID keeps using the single-track
route and never provokes the 403. Any other status still raises.

Callers see no difference beyond latency, which is why the fix belongs here
rather than in every caller.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant