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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtDNpayEYvJH7QuPswND3A
This commit is contained in:
Claude
2026-07-04 02:32:08 +00:00
parent 2be0c9f880
commit 9bb75d9bbd
2 changed files with 33 additions and 8 deletions
@@ -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)
@@ -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))
}
}