fix(client): abandon timed-out WebSocket handshakes without blocking (#113)#115
Conversation
…113) The connect timeout path called HttpClient.close(), which waits for the abandoned handshake to complete; against a server that accepts the TCP connection but never finishes the HTTP upgrade, the JDK client retries the GET on EOF, turning the configured timeout into an indefinite hang. The timeout path now cancels the handshake stage and uses shutdownNow(), so connect() throws promptly. Regression test connects to a silent accept-and-discard server with a 500ms timeout and bounds the wall clock. Closes #113 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe fix addresses a deadlock in WebSocket connection timeouts: when the server accepts TCP but ignores the HTTP upgrade, the JDK client retries on EOF while ChangesWebSocket Timeout Hang Fix
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
arcp-client/src/main/java/dev/arcp/client/WebSocketTransport.java (1)
120-140:⚠️ Potential issue | 🟠 Major | ⚡ Quick winClean up transport-owned resources on failed connect paths.
At Line 120 and Line 129,
connect(...)throws before releasingtransport.inbound/transport.inboundExecutorcreated at Line 83. Repeated timeout/failure retries can accumulate abandoned executors.Proposed fix
@@ } catch (java.util.concurrent.ExecutionException e) { Throwable cause = e.getCause() instanceof CompletionException ce ? ce.getCause() : e.getCause(); try { httpClient.close(); } catch (RuntimeException ignored) { // best-effort } + transport.inbound.close(); + transport.inboundExecutor.shutdown(); throw new IllegalStateException("WebSocket connect failed: " + cause, cause); } catch (java.util.concurrent.TimeoutException e) { @@ try { httpClient.shutdownNow(); } catch (RuntimeException ignored) { // best-effort } + transport.inbound.close(); + transport.inboundExecutor.shutdown(); throw new IllegalStateException("WebSocket connect timed out", e); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@arcp-client/src/main/java/dev/arcp/client/WebSocketTransport.java` around lines 120 - 140, connect(...) in WebSocketTransport throws on ExecutionException and TimeoutException without cleaning up the transport-owned resources created earlier, leaking transport.inbound and transport.inboundExecutor; in both catch blocks (the ExecutionException block handling "WebSocket connect failed" and the TimeoutException block handling "WebSocket connect timed out") ensure you explicitly close or release transport.inbound and shut down transport.inboundExecutor (e.g., call transport.inbound.close() if present and transport.inboundExecutor.shutdownNow()/shutdown with a short await), wrapping those calls in try/catch as best-effort cleanup before closing httpClient/shutting down the client and rethrowing the IllegalStateException so repeated failures don't accumulate abandoned executors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@arcp-client/src/main/java/dev/arcp/client/WebSocketTransport.java`:
- Around line 120-140: connect(...) in WebSocketTransport throws on
ExecutionException and TimeoutException without cleaning up the transport-owned
resources created earlier, leaking transport.inbound and
transport.inboundExecutor; in both catch blocks (the ExecutionException block
handling "WebSocket connect failed" and the TimeoutException block handling
"WebSocket connect timed out") ensure you explicitly close or release
transport.inbound and shut down transport.inboundExecutor (e.g., call
transport.inbound.close() if present and
transport.inboundExecutor.shutdownNow()/shutdown with a short await), wrapping
those calls in try/catch as best-effort cleanup before closing
httpClient/shutting down the client and rethrowing the IllegalStateException so
repeated failures don't accumulate abandoned executors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 80e97e4a-048e-41b6-a3bf-e3a2532082b2
📒 Files selected for processing (2)
arcp-client/src/main/java/dev/arcp/client/WebSocketTransport.javaarcp-client/src/test/java/dev/arcp/client/WebSocketConnectTimeoutTest.java
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The connect timeout path called HttpClient.close(), which waits for the
abandoned handshake to complete; against a server that accepts the TCP
connection but never finishes the HTTP upgrade, the JDK client retries
the GET on EOF, turning the configured timeout into an indefinite hang.
The timeout path now cancels the handshake stage and uses shutdownNow(),
so connect() throws promptly. Regression test connects to a silent
accept-and-discard server with a 500ms timeout and bounds the wall
clock.
Closes #113
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Summary by CodeRabbit
Bug Fixes
Tests