From 9bb75d9bbdc32190d281fef7debcb778592ad71f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 02:32:08 +0000 Subject: [PATCH] fix(quartz/desktop): close socket on connect failure; NODELAY for desktop pre-init client TcpNoDelaySocketFactory's connecting overloads used `socket().apply { connect(...) }`, which leaks the file descriptor if bind/connect throws (the JDK's connecting Socket constructors close on failure; ours didn't). Wrapped in a helper that closes on throw. OkHttp only calls the no-arg overload, so this guards any other direct caller. DesktopHttpClient's pre-init `simpleClient` (direct relay sockets opened before setInstance) now gets the same TcpNoDelaySocketFactory as directClient. failClosedClient is left as-is: it's a SOCKS client and OkHttp bypasses the socket factory for SOCKS proxies. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TtDNpayEYvJH7QuPswND3A --- .../desktop/network/DesktopHttpClient.kt | 6 ++++ .../sockets/okhttp/TcpNoDelaySocketFactory.kt | 35 ++++++++++++++----- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopHttpClient.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopHttpClient.kt index 6f4c290668..ea31213db2 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopHttpClient.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopHttpClient.kt @@ -154,6 +154,12 @@ class DesktopHttpClient( private val simpleClient: OkHttpClient by lazy { OkHttpClient .Builder() + // Direct relay sockets opened before setInstance() should + // get the same TCP_NODELAY as directClient — see + // TcpNoDelaySocketFactory. (failClosedClient is SOCKS, and + // OkHttp bypasses the socket factory for SOCKS proxies, so + // it doesn't need this.) + .socketFactory(TcpNoDelaySocketFactory) .connectTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS) .readTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS) .writeTimeout(BASE_TIMEOUT_SECONDS, TimeUnit.SECONDS) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/sockets/okhttp/TcpNoDelaySocketFactory.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/sockets/okhttp/TcpNoDelaySocketFactory.kt index 2f175a3b3b..6f581eb893 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/sockets/okhttp/TcpNoDelaySocketFactory.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/sockets/okhttp/TcpNoDelaySocketFactory.kt @@ -47,12 +47,31 @@ import javax.net.SocketFactory object TcpNoDelaySocketFactory : SocketFactory() { private fun socket() = Socket().apply { tcpNoDelay = true } + /** + * Runs [block] on a fresh NODELAY socket, closing it if the + * bind/connect throws — the JDK's connecting `Socket(...)` + * constructors do this, but our `Socket().apply { connect(...) }` + * form would otherwise leak the descriptor on a failed connect. + * (OkHttp only calls the no-arg overload, so this guards the other + * overloads for any direct caller.) + */ + private inline fun connecting(block: (Socket) -> Unit): Socket { + val s = socket() + try { + block(s) + } catch (t: Throwable) { + runCatching { s.close() } + throw t + } + return s + } + override fun createSocket(): Socket = socket() override fun createSocket( host: String?, port: Int, - ): Socket = socket().apply { connect(InetSocketAddress(host, port)) } + ): Socket = connecting { it.connect(InetSocketAddress(host, port)) } override fun createSocket( host: String?, @@ -60,15 +79,15 @@ object TcpNoDelaySocketFactory : SocketFactory() { localHost: InetAddress?, localPort: Int, ): Socket = - socket().apply { - bind(InetSocketAddress(localHost, localPort)) - connect(InetSocketAddress(host, port)) + connecting { + it.bind(InetSocketAddress(localHost, localPort)) + it.connect(InetSocketAddress(host, port)) } override fun createSocket( host: InetAddress?, port: Int, - ): Socket = socket().apply { connect(InetSocketAddress(host, port)) } + ): Socket = connecting { it.connect(InetSocketAddress(host, port)) } override fun createSocket( address: InetAddress?, @@ -76,8 +95,8 @@ object TcpNoDelaySocketFactory : SocketFactory() { localAddress: InetAddress?, localPort: Int, ): Socket = - socket().apply { - bind(InetSocketAddress(localAddress, localPort)) - connect(InetSocketAddress(address, port)) + connecting { + it.bind(InetSocketAddress(localAddress, localPort)) + it.connect(InetSocketAddress(address, port)) } }