@@ -295,6 +295,20 @@ def connectapi(self, path: str, **kwargs: Any) -> Any:
295295 """Wrapper for garth connectapi with error handling."""
296296 try :
297297 return self .garth .connectapi (path , ** kwargs )
298+ except AssertionError as e :
299+ # Handle Windows-specific OAuth token refresh issue
300+ # This can occur when garth tries to refresh tokens during API calls
301+ error_msg = str (e ).lower ()
302+ if "oauth" in error_msg and (
303+ "oauth1" in error_msg or "oauth2" in error_msg
304+ ):
305+ logger .error ("OAuth token refresh failed during API call." )
306+ raise GarminConnectAuthenticationError (
307+ "Token refresh failed. Please re-authenticate. "
308+ f"Original error: { e } "
309+ ) from e
310+ # Re-raise if it's a different AssertionError
311+ raise
298312 except (HTTPError , GarthHTTPError ) as e :
299313 # For GarthHTTPError, extract status from the wrapped HTTPError
300314 if isinstance (e , GarthHTTPError ):
@@ -369,12 +383,53 @@ def login(self, /, tokenstore: str | None = None) -> tuple[str | None, str | Non
369383 token1 = None
370384 token2 = None
371385
386+ # Try to load tokens from tokenstore if provided
387+ tokens_loaded = False
372388 if tokenstore :
373- if len (tokenstore ) > 512 :
374- self .garth .loads (tokenstore )
375- else :
376- self .garth .load (tokenstore )
377- else :
389+ try :
390+ if len (tokenstore ) > 512 :
391+ # Token data is provided directly as string (base64 encoded)
392+ self .garth .loads (tokenstore )
393+ else :
394+ # Tokenstore is a path - normalize it for cross-platform compatibility
395+ # This fixes Windows path issues where ~ expansion or path separators
396+ # might cause garth to not find all token files correctly
397+ tokenstore_path = Path (tokenstore ).expanduser ().resolve ()
398+ # Convert to string with normalized path separators
399+ normalized_path = str (tokenstore_path )
400+ logger .debug (
401+ f"Loading tokens from normalized path: { normalized_path } "
402+ )
403+ self .garth .load (normalized_path )
404+ tokens_loaded = True
405+ except AssertionError as e :
406+ # Handle Windows-specific OAuth token refresh issue
407+ # When garth tries to refresh OAuth2 tokens, it may fail with:
408+ # "AssertionError: OAuth1 token is required for OAuth2 refresh"
409+ # This can occur if token files are incomplete or path resolution failed
410+ error_msg = str (e ).lower ()
411+ if "oauth" in error_msg and (
412+ "oauth1" in error_msg or "oauth2" in error_msg
413+ ):
414+ logger .warning (
415+ "Token refresh failed (OAuth token mismatch). "
416+ "This may occur on Windows due to path or token file issues. "
417+ "Re-authentication required."
418+ )
419+ # Treat as invalid tokens - require re-authentication
420+ if not self .username or not self .password :
421+ raise GarminConnectAuthenticationError (
422+ "Stored tokens are invalid and credentials are required for re-authentication. "
423+ f"Original error: { e } "
424+ ) from e
425+ # Will fall through to credential-based login below
426+ tokens_loaded = False
427+ else :
428+ # Re-raise if it's a different AssertionError
429+ raise
430+
431+ # If tokens weren't loaded (or failed to load), use credentials
432+ if not tokens_loaded :
378433 # Validate credentials before attempting login
379434 if not self .username or not self .password :
380435 raise GarminConnectAuthenticationError (
0 commit comments