Repository navigation
Fix/auth refresh robustness 1 - #410
Merged
Merged
Conversation
TIDAL rotates refresh tokens: a refresh response can carry a new refresh_token and the previous one is invalidated server-side. token_refresh() updated the access token and expiry but never read the refresh_token field, so a session saved afterwards kept re-persisting the old token. Once TIDAL killed it, the next refresh failed and the user had to log in again.
When several threads (e.g. the paginated workers) hit an expired access token at the same time, each one POSTed the same refresh token. TIDAL rotates refresh tokens, so every POST after the first used a token that had just been invalidated, and the resulting broken lineage could end up persisted. Refreshes now go through a lock. A thread that gets the lock after another thread has already refreshed (the session's access token differs from the one its request was sent with) skips the refresh and just retries its request with the new token.
The access token was only refreshed after a 401 whose userMessage starts with exactly "The token has expired.". TIDAL has changed that wording before; when it doesn't match, the 401 propagates to the caller (seen with stream/download requests). When the expiry time is known, requests now refresh the token up front if it has expired or expires within a minute. The message based refresh stays as a fallback. A failed proactive refresh is logged and does not fail the request, since the token may still be accepted.
save_session_to_file() left expiry_time commented out, so a session restored with load_session_from_file() never knew when its access token expires and could only react to a 401 after the fact. Store it as an ISO 8601 string and restore it on load. Files written by older versions have no expiry_time and keep loading as before.
client_enable_hires() overwrites config.client_id/client_secret with the
PKCE pair. login_oauth() reads those same fields, but TIDAL rejects the
PKCE client on the device authorization endpoint ("Client is not a
Limited Input Device client"). Loading a PKCE session and then starting
a device login therefore failed.
Remember the previous pair when swapping and use it for the device login
flow. config.client_id keeps its current meaning for existing callers.
Session.__init__ used `config: Config = Config()`, which is evaluated once at import time. Every Session created without an explicit config therefore shared the same Config object, including its PKCE code_verifier and anything mutated later such as client_enable_hires(). Two sessions in one process (e.g. examples/transfer_favorites.py) would affect each other. Create a fresh Config per Session instead.
After refreshing the access token, basic_request() retried by calling itself. If TIDAL still answered "The token has expired." (for example because of clock skew), the retry refreshed and retried again, without limit, until the recursion limit was hit. Send the request through a helper and retry exactly once; a response that is still an error is returned to the caller.
TIDAL answers suspicious traffic with HTTP 429, or with HTTP 403 and a body mentioning abuse or a suspension. Requests sent while that is in effect only prolong it and can end in a permanent ban, but the library kept sending them and surfaced the 403 as a plain HTTPError. - AbuseDetected is raised for the 403 anti-abuse response, carrying retry_after and the reason. - Config(backoff=True) makes a Session stop sending requests after a 429 or an abuse response, for Retry-After seconds (60 s for a 429 and 30 minutes for abuse when absent). Requests made meanwhile raise TidalBackoffError with the remaining time, without reaching TIDAL. Session.request.backoff_remaining() exposes it. It is off by default, as it changes how long a rate limited session stays unusable. - Error bodies that are not JSON no longer break the error mapping, and a Retry-After given as an HTTP date no longer raises ValueError.
Requests sent while TIDAL is rate limiting or suspecting an account only prolong it, so holding them back is the safer behavior. Config.backoff now defaults to True; pass backoff=False to keep sending requests regardless. Only inspect the response for 429 and anti-abuse 403 when deciding whether to back off. This runs for every request now, and parsing Retry-After on successful responses was unnecessary work that also broke for mocked responses.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
token_refresh, previously users were logged out after a few days - tehkillerbee_login_oauthworks afterclient_enable_hireshas been called - tehkillerbee_Configobject - tehkillerbee_save_session_to_fileandload_session_from_filenow persistexpiry_time- tehkillerbee_AbuseDetected, raised when TIDAL answers with an anti-abuse HTTP 403 - tehkillerbee_Config(backoff=...), enabled by default: stop sending requests after a rate limit or abuse response and raiseTidalBackoffErroruntil it has passed. Passbackoff=Falseto disable - tehkillerbee_Retry-Aftergiven as a date no longer break error handling - tehkillerbee_