On a dual-stack network Plezy pinned API traffic and the notification websocket to IPv4 while the native Plex apps and the media stream used IPv6. dart:io looks up A before AAAA and connects in arrival order, so the AAAA address was never tried when the IPv4 path answered within 250 ms. Install an HttpClient.connectionFactory that does one lookup (keeping the resolver's RFC 6724 order), interleaves families per RFC 8305 and races candidates 250 ms apart. The task is returned before the lookup starts so connectionTimeout and cancel cover resolution. TLS is done in the factory since the SDK skips it once one is installed; proxied requests stay plain. The library-event and companion-remote websockets get the same client because WebSocket.connect otherwise builds its own. Supersedes #2233.
65 lines
2.8 KiB
Dart
65 lines
2.8 KiB
Dart
import 'dart:io' show HttpClient, Platform;
|
|
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/io_client.dart';
|
|
import 'package:win_http/win_http.dart';
|
|
|
|
import 'app_logger.dart';
|
|
import 'happy_eyeballs.dart';
|
|
import 'managed_http_client.dart';
|
|
import 'media_server_timeouts.dart';
|
|
|
|
final Set<String> _loggedPlatformClients = <String>{};
|
|
|
|
void _logPlatformClient(String platform, String client) {
|
|
if (!_loggedPlatformClients.add(client)) return;
|
|
appLogger.i('Platform HTTP client', error: {'platform': platform, 'client': client});
|
|
}
|
|
|
|
/// dart:io leaves TCP connects unbounded (Darwin retries SYNs for ~75 s) and
|
|
/// `package:http` cannot abort a request whose connection is still being
|
|
/// established, so every IOClient gets an explicit connect bound and
|
|
/// permission to force-close after a failed drain (#1972).
|
|
///
|
|
/// The pool is always tuned. Every surface that matters fans out — Plex and
|
|
/// Jellyfin home loads, artwork rails, tracker and Seerr traffic all issue
|
|
/// several concurrent requests per pass — and the dart:io defaults of 6
|
|
/// connections per host and a 15 s idle timeout cost a fresh TLS handshake per
|
|
/// request on a high-RTT or CDN link. On a 60-way artwork fan-out, 12/90 s
|
|
/// measured ~4x the default's throughput on Linux and ~2x on Android, so there
|
|
/// is no case left for the opt-in the tuning used to be.
|
|
ManagedHttpClient _createIoClient(String debugLabel) {
|
|
final httpClient = HttpClient()
|
|
..connectionTimeout = MediaServerTimeouts.connect
|
|
..maxConnectionsPerHost = 12
|
|
..idleTimeout = const Duration(seconds: 90)
|
|
..connectionFactory = happyEyeballsConnectionFactory;
|
|
return ManagedHttpClient(IOClient(httpClient), debugLabel: debugLabel, forceCloseOnDrainTimeout: true);
|
|
}
|
|
|
|
/// Every platform except Windows runs on the tuned dart:io client.
|
|
///
|
|
/// Windows keeps WinHTTP for the system proxy and the Schannel trust store.
|
|
/// Its `WINHTTP_OPTION_IPV6_FAST_FALLBACK` (#1128) is what
|
|
/// [happyEyeballsConnectionFactory] now supplies everywhere else.
|
|
///
|
|
/// Cronet and NSURLSession used to serve Android and Apple here. Both were
|
|
/// measured slower than this client on the request shapes Plezy actually
|
|
/// issues — Cronet by ~10x on LAN body throughput — so neither survived; see
|
|
/// issue #2140.
|
|
http.Client createPlatformClient() {
|
|
if (Platform.isWindows) {
|
|
try {
|
|
final client = WinHttpClient.defaultConfiguration();
|
|
_logPlatformClient('windows', 'WinHttpClient');
|
|
return ManagedHttpClient(client, debugLabel: 'WinHttpClient');
|
|
} catch (e, st) {
|
|
appLogger.w('WinHttpClient init failed, falling back to IOClient', error: e, stackTrace: st);
|
|
_logPlatformClient('windows', 'IOClient (fallback)');
|
|
return _createIoClient('IOClient (fallback)');
|
|
}
|
|
}
|
|
_logPlatformClient(Platform.operatingSystem, 'IOClient');
|
|
return _createIoClient('IOClient');
|
|
}
|