Skip to content

fix(audio_key): retry key requests on denial and timeout - #1763

Open
TheOneDenis wants to merge 1 commit into
librespot-org:devfrom
TheOneDenis:audio-key-retry
Open

TheOneDenis wants to merge 1 commit into
librespot-org:devfrom
TheOneDenis:audio-key-retry

Conversation

@TheOneDenis

Copy link
Copy Markdown

Summary

Retries audio key requests up to 3 times with backoff before failing the track, and cleans up timed-out pending entries so they no longer leak in the pending map.

Problem

Two failure modes are addressed:

  1. Intermittent key denial (Audio key error #1649): Spotify intermittently denies audio key requests for some accounts, even though the same request succeeds when retried shortly after. The denial is transient per request rather than a permanent account state — users report that playback recovers after a number of skipped tracks (e.g. error audio key 0 2 in Audio key error #1649).
  2. Timeout after AP reconnect: the 1500ms response timeout is tight immediately after an access-point reconnect while the new connection is still settling, failing tracks that would have played. This is the same situation previously addressed by the author-withdrawn fix(audio_key): retry on timeout and fail track cleanly on key error #1742 / fix(audio_key): retry on timeout and fail track cleanly on key error #1743.

Changes

  • core/src/audio_key.rs: request() retries the same request up to 3 times with a 1s backoff on both AesKey denial and timeout/channel-closure.
  • Pending entries are removed from the pending map on timeout/channel closure to prevent a leak.
  • The last error is returned after the retries are exhausted, preserving the Unavailable (denied) vs Aborted (timeout) distinction for callers.
  • CHANGELOG entry added under [Unreleased] → Fixed.

Testing

  • cargo build and cargo clippy clean, cargo fmt --all -- --check passes.
  • The retry behaviour was developed and verified on a setup that exhibits the transient denial described in Audio key error #1649 (tracks fail with error audio key 0 2, while subsequent identical requests succeed).

Copilot AI lite review requested due to automatic review settings September 18, 2026 08:40

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

Three moderate issues remain in audio-key cleanup and retry filtering.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR improves audio-key request reliability by retrying transient denials and timeouts, while cleaning up pending requests.

Changes:

  • Adds up to three attempts with one-second backoff.
  • Cleans pending entries after timeout or channel closure.
  • Documents the fix in CHANGELOG.md.
File summaries
File Summary Review findings
core/src/audio_key.rs Implements retry and cleanup logic. Three moderate findings remain: synchronous send failures can leak entries (3 votes); dropped callers can leak entries without RAII cleanup (1 vote); retries should be limited to transient denials to avoid delaying normal missing-key cases (1 vote).
CHANGELOG.md Adds the unreleased fix entry. No findings.
Review details

Suppressed comments (2)

core/src/audio_key.rs:98

  • The cleanup below only runs after the timeout future completes. If the caller drops this async request while it is waiting on rx (for example, when a pending player/preload load is replaced), the receiver is dropped but this sender remains in pending; with no response, that entry is never removed. Use a drop guard/RAII cleanup for each registered sequence.
            let (tx, rx) = oneshot::channel();

            let seq = self.lock(move |inner| {
                let seq = inner.sequence.get();
                inner.pending.insert(seq, tx);

core/src/audio_key.rs:109

  • This retries every AudioKeyError::AesKey, but the existing player intentionally uses a missing key as the normal path for unencrypted files (playback/src/player.rs:1108-1115, audio/src/decrypt.rs:24-25). Those files now incur two 1-second sleeps and three requests before playback can continue. Preserve the server's reason/code and retry only transient denials so permanent no-key responses remain immediate.
                Ok(Ok(Err(err))) => {
                    // The server denied the key for this request.
                    last_err = Some(err);
                }
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • 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 core/src/audio_key.rs
seq
});

self.send_key_request(seq, track, file)?;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — addressed in 7b929fe: the pending entry is now removed before propagating the send error.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in a519860: every attempt now registers its entry under a Drop guard, so the pending entry is removed however the request ends — including a caller dropping the future at an await point. The guard also replaces the manual cleanup on the send/timeout/channel paths.

On the retry filter: agreed — the server error code is now preserved in AudioKeyError::AesKey(u16) (marked breaking in the changelog) and only the transient code (0x0002, see #1649 reports of retries succeeding) is retried; permanent denials and no-key responses for unencrypted files return immediately.

@TheOneDenis
TheOneDenis force-pushed the audio-key-retry branch 2 times, most recently from 9da2e46 to 7b929fe Compare September 18, 2026 09:06
@hexyliae

Copy link
Copy Markdown

Still got an error:

[2026-09-19T12:00:21Z INFO  librespot] librespot 0.8.0 7b929fe (Built on 2026-09-18, Build ID: fVabpv8F, Profile: release)
[2026-09-19T12:00:21Z INFO  librespot_playback::mixer::softmixer] Mixing with softvol and volume control: Log(60.0)
[2026-09-19T12:00:21Z INFO  librespot_playback::convert] Converting with ditherer: tpdf
[2026-09-19T12:00:21Z INFO  librespot_playback::audio_backend::rodio] Using Rodio sink with format S16 and cpal host: ALSA
[2026-09-19T12:00:21Z INFO  librespot_playback::audio_backend::rodio] Using audio device: default
[2026-09-19T12:00:28Z INFO  librespot_core::session] Connecting to AP "ap-gew1.spotify.com:4070"
[2026-09-19T12:00:28Z INFO  librespot_core::session] Authenticated as '****************************' !
[2026-09-19T12:00:28Z INFO  librespot_core::session] Country: "**"
[2026-09-19T12:00:28Z INFO  librespot_core::spclient] Resolved "gew1-spclient.spotify.com:443" as spclient access point
[2026-09-19T12:00:28Z INFO  librespot_connect::spirc] active device is <> with session <5ZTYygRT07E4TJ8ehYshgR>
[2026-09-19T12:00:28Z WARN  librespot_connect::state::context] couldn't load context info because: context is not available. type: Default
[2026-09-19T12:00:28Z INFO  librespot_playback::player] Loading <Kool-Aid> with Spotify URI <spotify:track:0hpWmAB3L0OJ3VBeMkOQUu>
[2026-09-19T12:00:29Z ERROR librespot_core::audio_key] error audio key 0 1
[2026-09-19T12:00:29Z WARN  librespot_core::audio_key] audio key request failed, retrying in 1s (attempt 1/3)
[2026-09-19T12:00:29Z INFO  librespot_connect::spirc] session update: <Ok(NEW_SESSION)> for self, current session_id 04411364831a4eeeb5bcfb9a1d87e781, new session_id c506d3087b17ed7a27d19a21fdb65499
[2026-09-19T12:00:30Z ERROR librespot_core::audio_key] error audio key 0 1
[2026-09-19T12:00:30Z WARN  librespot_core::audio_key] audio key request failed, retrying in 1s (attempt 2/3)
[2026-09-19T12:00:31Z ERROR librespot_core::audio_key] error audio key 0 1
[2026-09-19T12:00:31Z WARN  librespot_playback::player] Unable to load key, continuing without decryption: Service unavailable { audio key error }
[2026-09-19T12:00:31Z ERROR librespot_playback::player] Unable to read audio file: Symphonia Decoder Error: end of stream
[2026-09-19T12:00:31Z ERROR librespot_playback::player] Skipping to next track, unable to load track <SpotifyUri("spotify:track:0hpWmAB3L0OJ3VBeMkOQUu")>: ()
[2026-09-19T12:00:32Z INFO  librespot_playback::player] Loading <Top 10 staTues tHat CriEd bloOd> with Spotify URI <spotify:track:7g4hIeYVk3h5qQXb9RLeXP>
[2026-09-19T12:00:32Z ERROR librespot_core::audio_key] error audio key 0 1
[2026-09-19T12:00:32Z WARN  librespot_core::audio_key] audio key request failed, retrying in 1s (attempt 1/3)
[2026-09-19T12:00:33Z ERROR librespot_core::audio_key] error audio key 0 1
[2026-09-19T12:00:33Z WARN  librespot_core::audio_key] audio key request failed, retrying in 1s (attempt 2/3)
[2026-09-19T12:00:34Z ERROR librespot_core::audio_key] error audio key 0 1
[2026-09-19T12:00:34Z WARN  librespot_playback::player] Unable to load key, continuing without decryption: Service unavailable { audio key error }
[2026-09-19T12:00:35Z ERROR librespot_playback::player] Unable to read audio file: Symphonia Decoder Error: end of stream
[2026-09-19T12:00:35Z ERROR librespot_playback::player] Skipping to next track, unable to load track <SpotifyUri("spotify:track:7g4hIeYVk3h5qQXb9RLeXP")>: ()

Spotify intermittently denies audio key requests for some accounts even
though the same request succeeds when retried shortly after (issue librespot-org#1649).
Responses can also time out right after an access-point reconnect while the
new connection is still settling.

Retry the same key request up to 3 times with backoff, but only for the
transient denial code (0x0002) and timeouts; permanent denials and
no-key responses for unencrypted files return immediately so the normal
playback path stays fast. To make this distinction possible the Spotify
server error code is now carried in AudioKeyError::AesKey (breaking).

The pending entry is guarded by a Drop guard, so it is removed when the
request finishes or is dropped at any await point, not only on the paths
that explicitly clean up.
@TheOneDenis

Copy link
Copy Markdown
Author

Thanks for testing @hexyliae! Your log confirms the retry path works ( …), but what you're hitting looks like the persistent variant of #1649 (error code 0x0001): the server denies every request for that account/track combination regardless of retries — the same behaviour multiple users report in the issue thread. Your log actually helps pin down the distinction: retesting users see 0x0002 (transient — retry succeeds), while permanently affected accounts get 0x0001 (never succeeds).

That distinction is now encoded in a519860: only the transient code is retried (up to 3x with backoff), permanent denials return immediately as before, so your case would fail fast rather than burning 3 requests per track. Unfortunately no client-side change can lift a persistent server-side denial — that part needs Spotify, or works again as mysteriously as it broke (per reports in #1649).

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