fix(media): HTTP/2 keepalive ping to stop stale-connection image stalls

The media OkHttp client keeps 32 connections warm in a 5-minute pool but,
unlike the relay client, set no pingInterval. Hosts like blossom.primal.net
silently drop idle HTTP/2 connections between feed-scroll bursts. OkHttp then
pulls a dead connection from the pool and the request stalls until the read
timeout (30s wifi / 90s mobile), which users see as "the first image after a
pause takes forever".

Device MediaHttp logs showed the signature repeatedly:
  blossom.primal.net total=30000ms ttfb=-1ms conn=reused error=SocketTimeout

Add a 10s HTTP/2 keepalive ping so dead pooled connections are detected in
seconds and retryOnConnectionFailure re-issues on a fresh one, mirroring what
the relay client already does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-06-20 10:24:50 -04:00
co-authored by Claude Opus 4.8
parent ee41ac7b7e
commit 1223f5b832
@@ -75,6 +75,14 @@ class OkHttpClientFactory(
.connectionPool(connectionPool)
.dns(dns)
.eventListenerFactory(MediaCallEventListenerFactory(dispatcher, connectionPool, dns))
// Some media hosts (e.g. blossom.primal.net) silently drop idle HTTP/2
// connections between the bursts of a feed scroll. Without a keepalive
// ping, OkHttp can pull such a dead connection from the 5-min pool and
// the request stalls until the read timeout (30s wifi / 90s mobile),
// which shows up as "the first image after a pause takes forever".
// An HTTP/2 ping detects the dead connection in seconds and lets
// retryOnConnectionFailure re-issue the request on a fresh one.
.pingInterval(Duration.ofSeconds(HTTP2_PING_INTERVAL_SECS))
.followRedirects(true)
.followSslRedirects(true)
.addInterceptor(DefaultContentTypeInterceptor(userAgent))
@@ -128,4 +136,11 @@ class OkHttpClientFactory(
}
fun buildLocalSocksProxy(port: Int?) = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port ?: DEFAULT_SOCKS_PORT))
companion object {
// HTTP/2 keepalive ping for pooled media connections. Short enough to
// evict a silently-dropped connection well before the read timeout would
// otherwise stall a request for the full 30s/90s.
const val HTTP2_PING_INTERVAL_SECS: Long = 10
}
}