From 383351d7696ca99430d2fdaa81a1ba0d6a89d46b Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:58:44 +0100 Subject: [PATCH] fix: detect invalid tokens and prevent startup hangs close #553 --- lib/main.dart | 20 ++++++++++++++---- lib/providers/offline_mode_provider.dart | 4 +++- lib/services/multi_server_manager.dart | 15 +++++++------- lib/services/plex_client.dart | 13 +++++++++++- lib/services/server_registry.dart | 26 ++++++++++++++++++------ 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 08acff74..84820f9a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -378,13 +378,25 @@ class _SetupScreenState extends State { final storage = await StorageService.getInstance(); final registry = ServerRegistry(storage); - // Check network connectivity early to fast-path airplane mode - final connectivityResult = await Connectivity().checkConnectivity(); + // Check network connectivity early to fast-path airplane mode. + // Timeout guards against connectivity_plus hanging on some Android TV devices after force-close. + final connectivityResult = await Connectivity() + .checkConnectivity() + .timeout(const Duration(seconds: 3), onTimeout: () => [ConnectivityResult.other]); final hasNetwork = !connectivityResult.contains(ConnectivityResult.none); if (hasNetwork) { - // Refresh servers from API to get updated connection info (IPs may change) - await registry.refreshServersFromApi(); + // Refresh servers from API to get updated connection info (IPs may change). + // If the stored token is invalid (e.g. after removing a Plex profile PIN), + // redirect to AuthScreen so the user can re-authenticate. + final refreshResult = await registry.refreshServersFromApi(); + if (refreshResult == ServerRefreshResult.authError) { + await storage.clearCredentials(); + if (mounted) { + Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const AuthScreen())); + } + return; + } } // Load all configured servers diff --git a/lib/providers/offline_mode_provider.dart b/lib/providers/offline_mode_provider.dart index fa23c45e..ef1a9068 100644 --- a/lib/providers/offline_mode_provider.dart +++ b/lib/providers/offline_mode_provider.dart @@ -28,7 +28,9 @@ class OfflineModeProvider extends ChangeNotifier { /// Updates network and server connection flags Future _updateConnectionFlags() async { - final connectivityResult = await Connectivity().checkConnectivity(); + final connectivityResult = await Connectivity() + .checkConnectivity() + .timeout(const Duration(seconds: 3), onTimeout: () => [ConnectivityResult.other]); _hasNetworkConnection = !connectivityResult.contains(ConnectivityResult.none); _hasServerConnection = _serverManager.onlineServerIds.isNotEmpty; } diff --git a/lib/services/multi_server_manager.dart b/lib/services/multi_server_manager.dart index 8e6b99d6..9afd4282 100644 --- a/lib/services/multi_server_manager.dart +++ b/lib/services/multi_server_manager.dart @@ -288,7 +288,9 @@ class MultiServerManager { } } - /// Test connection health for all servers + /// Test connection health for all servers. + /// Uses [PlexClient.isHealthy] which checks for HTTP 200, so servers with + /// invalid tokens (401) are correctly reported as offline. Future checkServerHealth() async { appLogger.d('Checking health for ${_clients.length} servers'); @@ -296,13 +298,10 @@ class MultiServerManager { final serverId = entry.key; final client = entry.value; - try { - // Simple ping by fetching server identity - await client.getServerIdentity(); - updateServerStatus(serverId, true); - } catch (e) { - appLogger.w('Server $serverId health check failed: $e'); - updateServerStatus(serverId, false); + final healthy = await client.isHealthy(); + updateServerStatus(serverId, healthy); + if (!healthy) { + appLogger.w('Server $serverId health check failed'); } }); diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index c1cc4bd6..86eb09c6 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -233,7 +233,7 @@ class PlexClient { final response = await dio.get('/', options: Options(headers: {'X-Plex-Token': token})); stopwatch.stop(); - final success = response.statusCode == 200 || response.statusCode == 401; + final success = response.statusCode == 200; return ConnectionTestResult( success: success, @@ -371,6 +371,17 @@ class PlexClient { return response.data; } + /// Check if the server connection is healthy (reachable AND authenticated). + /// Returns true only if the server responds with HTTP 200. + Future isHealthy() async { + try { + final response = await _dio.get('/identity'); + return response.statusCode == 200; + } catch (e) { + return false; + } + } + /// Get library sections /// Returns libraries automatically tagged with this client's serverId and serverName Future> getLibraries() async { diff --git a/lib/services/server_registry.dart b/lib/services/server_registry.dart index 954dfea3..3be80d78 100644 --- a/lib/services/server_registry.dart +++ b/lib/services/server_registry.dart @@ -1,9 +1,13 @@ import 'dart:convert'; +import 'package:dio/dio.dart'; + import '../utils/app_logger.dart'; import 'plex_auth_service.dart'; import 'storage_service.dart'; +enum ServerRefreshResult { success, networkError, authError, noToken } + /// Centralized server configuration registry /// Manages which servers are available and their configurations class ServerRegistry { @@ -95,13 +99,15 @@ class ServerRegistry { appLogger.i('Cleared all servers from registry'); } - /// Refresh servers from Plex API and update storage - /// This updates connection info (IPs, ports) that may have changed - Future refreshServersFromApi() async { + /// Refresh servers from Plex API and update storage. + /// This updates connection info (IPs, ports) that may have changed. + /// Returns [ServerRefreshResult.authError] when the stored token is rejected + /// (e.g. after removing a Plex profile PIN), so the caller can redirect to re-auth. + Future refreshServersFromApi() async { final token = _storage.getPlexToken(); if (token == null || token.isEmpty) { appLogger.d('No Plex token available, skipping server refresh'); - return; + return ServerRefreshResult.noToken; } try { @@ -111,7 +117,7 @@ class ServerRegistry { if (freshServers.isEmpty) { appLogger.w('API returned no servers, keeping existing data'); - return; + return ServerRefreshResult.success; } // Get existing servers to preserve any local-only data @@ -133,9 +139,17 @@ class ServerRegistry { await saveServers(updatedServers); appLogger.i('Refreshed ${updatedServers.length} servers from API'); + return ServerRefreshResult.success; + } on DioException catch (e) { + if (e.response?.statusCode == 401) { + appLogger.w('Plex token is invalid (401), re-authentication required'); + return ServerRefreshResult.authError; + } + appLogger.w('Failed to refresh servers from API, using cached data', error: e); + return ServerRefreshResult.networkError; } catch (e, stackTrace) { appLogger.w('Failed to refresh servers from API, using cached data', error: e, stackTrace: stackTrace); - // Don't rethrow - we can continue with cached servers + return ServerRefreshResult.networkError; } } }