fix: detect invalid tokens and prevent startup hangs

close #553
This commit is contained in:
edde746
2026-02-25 23:59:19 +01:00
parent 0f4920cc18
commit 383351d769
5 changed files with 58 additions and 20 deletions
+16 -4
View File
@@ -378,13 +378,25 @@ class _SetupScreenState extends State<SetupScreen> {
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
+3 -1
View File
@@ -28,7 +28,9 @@ class OfflineModeProvider extends ChangeNotifier {
/// Updates network and server connection flags
Future<void> _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;
}
+7 -8
View File
@@ -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<void> 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');
}
});
+12 -1
View File
@@ -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<bool> 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<List<PlexLibrary>> getLibraries() async {
+20 -6
View File
@@ -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<void> 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<ServerRefreshResult> 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;
}
}
}