diff --git a/lib/utils/http_client.dart b/lib/utils/http_client.dart index 8a8fe8f1..0158e3df 100644 --- a/lib/utils/http_client.dart +++ b/lib/utils/http_client.dart @@ -1,14 +1,53 @@ +import 'dart:io'; +import 'dart:typed_data'; + import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; import 'package:dio_http2_adapter/dio_http2_adapter.dart'; +import 'app_logger.dart'; + /// Shared HTTP/2 connection pool. All Dio instances that go through /// [createHttp2Adapter] reuse the same pool, so connections to the same /// host are multiplexed instead of duplicated. final _connectionManager = ConnectionManager(idleTimeout: const Duration(seconds: 15)); +final _http1Fallback = IOHttpClientAdapter(); -/// Returns an [Http2Adapter] backed by the shared connection pool. -HttpClientAdapter createHttp2Adapter() => Http2Adapter(_connectionManager); +/// Returns an [HttpClientAdapter] that tries HTTP/2 first and falls back +/// to HTTP/1.1 when the server rejects h2 ALPN negotiation. +HttpClientAdapter createHttp2Adapter() => _Http2WithFallbackAdapter(Http2Adapter(_connectionManager)); /// Shared [Dio] instance for ad-hoc HTTP requests that don't go through /// [PlexClient]. Reuses the global HTTP/2 connection pool. final httpClient = Dio()..httpClientAdapter = createHttp2Adapter(); + +class _Http2WithFallbackAdapter implements HttpClientAdapter { + _Http2WithFallbackAdapter(this._h2); + + final Http2Adapter _h2; + + static final _http1Hosts = {'plex.tv'}; + + @override + Future fetch( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, + ) async { + if (_http1Hosts.contains(options.uri.host)) { + return _http1Fallback.fetch(options, requestStream, cancelFuture); + } + try { + return await _h2.fetch(options, requestStream, cancelFuture); + } on HandshakeException { + appLogger.d('H2 handshake failed for ${options.uri.host}, falling back to HTTP/1.1'); + _http1Hosts.add(options.uri.host); + return _http1Fallback.fetch(options, requestStream, cancelFuture); + } + } + + @override + void close({bool force = false}) { + _h2.close(force: force); + } +}