From e1c9ce6f7163d0cfd2bf143de2733e746d9c9f2f Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:37:20 +0200 Subject: [PATCH] fix: gate http fallback for hostname https, throw on non-2xx in failover close #900 --- lib/services/plex_auth_service.dart | 29 ++++++++++++----------------- lib/services/plex_client.dart | 15 +++++++++++++-- lib/utils/plex_http_client.dart | 13 +++++++++++++ 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/lib/services/plex_auth_service.dart b/lib/services/plex_auth_service.dart index a6ec3219..1858d96d 100644 --- a/lib/services/plex_auth_service.dart +++ b/lib/services/plex_auth_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show InternetAddress; import 'package:uuid/uuid.dart'; import 'storage_service.dart'; import 'plex_client.dart'; @@ -8,7 +9,6 @@ import '../models/user_switch_response.dart'; import '../utils/app_logger.dart'; import '../utils/connection_constants.dart'; import '../utils/plex_http_client.dart'; -import '../utils/plex_http_exception.dart'; /// Redacts the middle of an IP address or hostname for safe logging. /// E.g. `192.168.1.50` → `192.***.***.50`, `my.server.example.com` → `my.***.***. com`. @@ -87,17 +87,7 @@ class PlexAuthService { return _http.get('$_plexApiBase/user', headers: _getCommonHeaders(authToken: authToken)); } - /// Throw [PlexHttpException] if the response indicates a client/server error. - void _checkStatus(PlexResponse response) { - if (response.statusCode >= 400) { - throw PlexHttpException( - type: PlexHttpErrorType.unknown, - statusCode: response.statusCode, - responseData: response.data, - message: 'HTTP ${response.statusCode}', - ); - } - } + void _checkStatus(PlexResponse response) => throwIfHttpError(response); /// Verify if a plex.tv token is valid Future verifyToken(String authToken) async { @@ -303,8 +293,8 @@ class PlexServer { final connection = PlexConnection.fromJson(c as Map); connections.add(connection); - // Generate HTTP fallback for HTTPS connections - if (connection.protocol == 'https') { + if (connection.protocol == 'https' && + (connection.uri.contains('.plex.direct') || _isIpLiteral(connection.address))) { connections.add(connection.toHttpFallback()); } } catch (e) { @@ -650,9 +640,7 @@ class PlexServer { final isHttps = connection.protocol == 'https'; addCandidate(connection, connection.uri, isPlexDirect, isHttps); - // For HTTPS connections, also add HTTP direct IP as fallback - // This provides backward compatibility and fallback for cert issues - if (isHttps) { + if (isHttps && (isPlexDirect || _isIpLiteral(connection.address))) { addCandidate(connection, connection.httpDirectUrl, false, false); } } @@ -829,6 +817,13 @@ class PlexServer { return entries.first.key; } + /// True when the address is a raw IP (no hostname → no reverse proxy → HTTP + /// fallback on an HTTPS port is safe to try). + static bool _isIpLiteral(String address) { + final bare = address.startsWith('[') && address.endsWith(']') ? address.substring(1, address.length - 1) : address; + return InternetAddress.tryParse(bare) != null; + } + /// Returns true if the address is known to be unreachable from external /// clients (IPv6 link-local or all-zeros). static bool _isUnreachableAddress(String address) { diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 3dc22293..78118d4f 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -213,7 +213,15 @@ class PlexClient { }) async { final gen = _endpointManager?.generation; try { - return await _http.get(path, queryParameters: queryParameters, headers: headers, timeout: timeout, abort: abort); + final response = await _http.get( + path, + queryParameters: queryParameters, + headers: headers, + timeout: timeout, + abort: abort, + ); + throwIfHttpError(response); + return response; } on PlexHttpException catch (e) { if (!_shouldAttemptFailover(e) || _failoverSwitching || @@ -246,6 +254,7 @@ class PlexClient { timeout: timeout, abort: abort, ); + throwIfHttpError(response); appLogger.i('Endpoint failover retry succeeded', error: {'newEndpoint': nextBaseUrl}); await _onEndpointChanged?.call(nextBaseUrl); return response; @@ -256,9 +265,11 @@ class PlexClient { } bool _shouldAttemptFailover(PlexHttpException e) { + final sc = e.statusCode; return e.type == PlexHttpErrorType.connectionTimeout || e.type == PlexHttpErrorType.receiveTimeout || - e.type == PlexHttpErrorType.connectionError; + e.type == PlexHttpErrorType.connectionError || + (sc != null && sc >= 500 && sc <= 599); } /// Fetch /media/providers and parse libraries + EPG providers from the response. diff --git a/lib/utils/plex_http_client.dart b/lib/utils/plex_http_client.dart index ade64b56..8e074088 100644 --- a/lib/utils/plex_http_client.dart +++ b/lib/utils/plex_http_client.dart @@ -27,6 +27,19 @@ class PlexResponse { PlexResponse({required this.statusCode, this.data, required this.headers}); } +/// Throw [PlexHttpException] for non-2xx responses so callers don't blindly +/// cast HTML/text error bodies to `Map`. +void throwIfHttpError(PlexResponse r) { + if (r.statusCode >= 400) { + throw PlexHttpException( + type: PlexHttpErrorType.unknown, + statusCode: r.statusCode, + responseData: r.data, + message: 'HTTP ${r.statusCode}', + ); + } +} + /// Abort controller for cancelling in-flight HTTP requests. /// /// Uses the `package:http` [AbortableRequest] mechanism so the underlying