Skip to content

audio: fall back to the next CDN URL when a fetch returns a non-206 status#1722

Open
dmeiselman wants to merge 1 commit into
librespot-org:devfrom
dmeiselman:cdn-failover-on-non-206
Open

audio: fall back to the next CDN URL when a fetch returns a non-206 status#1722
dmeiselman wants to merge 1 commit into
librespot-org:devfrom
dmeiselman:cdn-failover-on-non-206

Conversation

@dmeiselman

Copy link
Copy Markdown

Problem

Spotify hands librespot several CDN URLs for each track and expects the client to try the next one if a URL doesn't work. Right now, librespot only moves on to the next URL when a download fails at the connection level (timeout, DNS, TLS, etc.).

But a CDN edge can accept the connection and still answer with an HTTP error — e.g. a 500 Internal Server Error from an edge node that's being drained. To librespot that counts as a "successful" response, so it stops on that first broken URL and never tries the others. The track fails to load, and playback never starts.

This currently breaks playback completely for some accounts: Spotify is handing out a bad edge as the first URL, so every track dies on the first try even though working URLs were right there in the list.

This fix builds on #1524, which added CDN-URL fallback but only for connection-level errors. This change extends that same fallback to cover HTTP error responses too.

Evidence

For one track, storage-resolve returned 4 CDN URLs. Fetching each directly with a range request (curl):

audio-fa-del-874.spotifycdn.com - 500 ← the only one librespot tried
audio-cf.spotifycdn.com - 206
audio-fa.scdn.co - 206
audio4-ak.spotifycdn.com - 206

librespot's log on the unpatched build:

DEBUG librespot_audio::fetch] Opening audio file expected partial content but got: 500 Internal Server Error
ERROR librespot_playback::player] Unable to load encrypted file: Error { kind: FailedPrecondition, error: StatusCode(500) }

It tried the broken URL, treated the 500 as a final answer, and gave up — never attempting the three working URLs.

Fix

A successful fetch is now defined as one that actually returns 206 Partial Content (what a range request should return). Anything else — a 500, or any other status — is logged and treated like the other failures, so the loop continues to the next CDN URL instead of stopping.

The change is in the existing URL-retry loop in audio/src/fetch/mod.rs (AudioFileStreaming::open): the "is this 206?" check, which previously ran after the loop had already committed to one URL, now runs inside the loop so a non-206 response falls through to the next candidate.

Testing

Built from this branch and played tracks against the same account that was failing:

  • Before: 0 tracks loaded (every one hit the 500 and gave up).
  • After: 31/31 tracks loaded and played — librespot skipped the 500 edge and streamed from the next 206 URL automatically.

Also verified end-to-end through the spotty helper / Lyrion Music Server, where this bug was originally observed: full tracks decode and play.

AudioFileStreaming::open() resolves several CDN URLs from storage-resolve and loops over them to find one that streams, but the loop only treats a transport error (the Err arm) as a reason to try the next URL. An HTTP error status such as 500 is a successful response at the transport layer, so the Ok(_) arm matches and the loop breaks on that first URL. The StatusCode::PARTIAL_CONTENT check then runs AFTER the loop and returns an error, so the remaining (working) CDN URLs are never attempted and playback fails (surfacing as "Unable to load encrypted file: FailedPrecondition").

Reproducible against Spotify today: storage-resolve returns multiple CDN URLs where the first host (e.g. audio-fa-del-874.spotifycdn.com) returns 500 while the others (audio-cf.spotifycdn.com, audio-fa.scdn.co, audio4-ak.spotifycdn.com) return 206. Every track fails even though working URLs were provided in the same response.

Move the 206 acceptance into the loop: only break when a URL responds with PARTIAL_CONTENT; otherwise log the unexpected status and fall through to the next URL. The post-loop status check is kept as a safety net.

Tested against a live Premium account: before, 0 tracks loaded (every track returned 500 on the first URL); after, tracks load by failing over from the 500 edge to a 206 one.

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.

Pull request overview

Extends the existing CDN URL fallback logic in AudioFileStreaming::open so that librespot also retries the next CDN URL when the HTTP response is not 206 Partial Content, not just when connection-level errors occur. This aligns the client behavior with Spotify’s expectation that multiple CDN URLs are tried until a working one is found.

Changes:

  • Treat non-206 Partial Content HTTP responses as a failed attempt and continue to the next CDN URL.
  • Log non-206 responses as retryable failures during initial streaming open.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread audio/src/fetch/mod.rs
Comment on lines 469 to +477
match streamer_result {
Ok(r) => {
Ok(r) if r.status() == StatusCode::PARTIAL_CONTENT => {
response_streamer_url = Some((r, streamer, url));
break;
}
Ok(r) => warn!(
"Fetching {url} returned {} (expected 206 Partial Content), trying next",
r.status()
),
@milnivlek

Copy link
Copy Markdown

This is very timely, as a Spotify-side issue apparently arose this morning which this PR should help mitigate. #1723 (comment)

Please merge!

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.

3 participants