From fd701e5d1f8b4d4e8a65cc1078924f5ce326b282 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:12:18 +0200 Subject: [PATCH] fix: restrict failover to working endpoint network class close #902 --- lib/services/plex_auth_service.dart | 37 ++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/services/plex_auth_service.dart b/lib/services/plex_auth_service.dart index 463b9e33..0e3ec0e4 100644 --- a/lib/services/plex_auth_service.dart +++ b/lib/services/plex_auth_service.dart @@ -238,6 +238,9 @@ class PlexAuthService { } } +/// Classification of an endpoint URL by the server's published connection type. +enum PlexNetworkClass { local, remote, relay, unknown } + /// Helper class to track connection candidates during testing class _ConnectionCandidate { final PlexConnection connection; @@ -591,7 +594,14 @@ class PlexServer { return null; } - List<_ConnectionCandidate> _buildPrioritizedCandidates({Set? excludeUrls}) { + /// Classify [url] against the server's published connections. Returns + /// [PlexNetworkClass.unknown] when the URL doesn't match any known endpoint + /// (e.g. a manually-entered custom URL). + PlexNetworkClass networkClassForUrl(String url) { + return _candidateForUrl(url)?.connection.networkClass ?? PlexNetworkClass.unknown; + } + + List<_ConnectionCandidate> _buildPrioritizedCandidates({Set? excludeUrls, PlexNetworkClass? restrictTo}) { final seen = {}; if (excludeUrls != null) { seen.addAll(excludeUrls); @@ -641,19 +651,34 @@ class PlexServer { } } - return [...httpsLocal, ...httpsRemote, ...httpsRelay, ...httpLocal, ...httpRemote, ...httpRelay]; + final all = [...httpsLocal, ...httpsRemote, ...httpsRelay, ...httpLocal, ...httpRemote, ...httpRelay]; + + // Failover reachability filter. After discovery, failover should stay within + // endpoint families plausible for the current session: when a remote or relay + // endpoint is what's working, LAN-only endpoints are unreachable from off-LAN + // clients and only pollute the retry chain (see GH #902). Reconnect / + // reoptimization widens the search again via _reoptimizeServer. Local is kept + // conservative — it may still be reachable over VPN/routed-LAN, but those + // setups re-widen on the next connectivity event. + if (restrictTo == PlexNetworkClass.remote || restrictTo == PlexNetworkClass.relay) { + final filtered = all.where((c) => c.connection.networkClass != PlexNetworkClass.local).toList(); + if (filtered.isNotEmpty) return filtered; + } + return all; } List prioritizedEndpointUrls({String? preferredFirst}) { final urls = []; final exclude = {}; + PlexNetworkClass? restrictTo; if (preferredFirst != null && preferredFirst.isNotEmpty) { urls.add(preferredFirst); exclude.add(preferredFirst); + restrictTo = networkClassForUrl(preferredFirst); } - final candidates = _buildPrioritizedCandidates(excludeUrls: exclude); + final candidates = _buildPrioritizedCandidates(excludeUrls: exclude, restrictTo: restrictTo); urls.addAll(candidates.map((candidate) => candidate.url)); return urls; } @@ -927,6 +952,12 @@ class PlexConnection { return 'Remote'; } + PlexNetworkClass get networkClass { + if (relay) return PlexNetworkClass.relay; + if (local) return PlexNetworkClass.local; + return PlexNetworkClass.remote; + } + /// Create an HTTP fallback version of this HTTPS connection /// This allows testing HTTP when HTTPS is unavailable (e.g., certificate issues) PlexConnection toHttpFallback() {