diff --git a/tidaldy/src/lib.rs b/tidaldy/src/lib.rs index 846bf10..cf918d2 100644 --- a/tidaldy/src/lib.rs +++ b/tidaldy/src/lib.rs @@ -344,6 +344,11 @@ impl Client { /// Refreshes the access token if it is expired or about to expire. /// Without this, long-running sessions ended up with an expired token /// and every track fetch failed, silently stopping playback. + /// + /// A failed proactive refresh never fails the request: the current + /// access token may still be valid (stale `expires_after` metadata from + /// an old config, for example), and `make_request`'s 401-retry remains + /// the backstop for a token that is actually dead. async fn ensure_fresh_token(&self) -> Result<(), ClientError> { let login = self.login_snapshot(); let Some(expires_after) = login.expires_after else { @@ -354,7 +359,30 @@ impl Client { return Ok(()); } info!("access token expired or expiring soon"); - self.force_refresh_token().await + match self.force_refresh_token().await { + Ok(()) => Ok(()), + Err(err @ ClientError::AuthError(_)) => { + // The refresh token is permanently unusable. Stop trying + // proactively so every request doesn't hammer the endpoint; + // a new device login (server restart) is the only cure once + // the access token dies. + warn!( + "stored refresh token was rejected and can never work; \ + continuing with the current access token — restart the \ + server to re-login once it expires: {err}" + ); + let mut login = match self.login.write() { + Ok(login) => login, + Err(poisoned) => poisoned.into_inner(), + }; + login.expires_after = None; + Ok(()) + } + Err(err) => { + warn!("token refresh failed transiently, continuing with the current token: {err}"); + Ok(()) + } + } } /// Performs an authenticated GET against the hifi API. @@ -689,12 +717,20 @@ impl Client { if status.is_success() { let res = req.json::().await?; Ok(res) - } else { + } else if status.is_client_error() { + // A 4xx means the endpoint understood us and rejected the token + // (e.g. it was issued by a client that no longer exists). This + // cannot succeed on retry — only a new device login helps. let body = req.text().await.unwrap_or_default(); let snippet: String = body.chars().take(300).collect(); Err(ClientError::AuthError(format!( "token refresh returned {status}: {snippet}" ))) + } else { + let body = req.text().await.unwrap_or_default(); + let snippet: String = body.chars().take(300).collect(); + error!("token refresh returned {status}: {snippet}"); + Err(ClientError::ApiError(status.as_u16())) } } #[instrument(skip(self))]