From e5e6625ea594aae2c3959168f86159d5bf071b5f Mon Sep 17 00:00:00 2001 From: Capybara Date: Sat, 26 Sep 2026 11:26:12 +0800 Subject: [PATCH 1/5] Handle fatal Errors in WebSocketClient read loop (#1460) --- .../client/WebSocketClient.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/java_websocket/client/WebSocketClient.java b/src/main/java/org/java_websocket/client/WebSocketClient.java index 0e38326d..6ea91ba0 100644 --- a/src/main/java/org/java_websocket/client/WebSocketClient.java +++ b/src/main/java/org/java_websocket/client/WebSocketClient.java @@ -201,7 +201,6 @@ public WebSocketClient(URI serverUri, Draft protocolDraft, Map h /** * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The * channel does not attampt to connect automatically. The connection will be established once you - * call connect. * * @param serverUri the server URI to connect to * @param protocolDraft The draft which should be used for this connection @@ -446,7 +445,7 @@ public void close() { * * @throws InterruptedException Thrown when the threads get interrupted */ - public void closeBlocking() throws InterruptedException { + public boolean closeBlocking() throws InterruptedException { close(); closeLatch.await(); } @@ -565,13 +564,20 @@ public void run() { // this catch case covers internal errors only and indicates a bug in this websocket implementation onError(e); engine.closeConnection(CloseFrame.ABNORMAL_CLOSE, e.getMessage()); + } catch (VirtualMachineError | ThreadDeath | LinkageError e) { + // Fatal Errors rethrown by WebSocketImpl#decodeFrames must surface via onError and + // close the connection instead of silently terminating the connect/read thread. + // See https://github.com/TooTallNate/Java-WebSocket/issues/1460 + onError(new Exception(e)); + engine.closeConnection(CloseFrame.UNEXPECTED_CONDITION, + "Got error " + e.getClass().getName()); } } private void upgradeSocketToSSL() throws NoSuchAlgorithmException, KeyManagementException, IOException { SSLSocketFactory factory; - // Prioritise the provided socketfactory + // Prioritse the provided socketfactory // Helps when using web debuggers like Fiddler Classic if (socketFactory instanceof SSLSocketFactory) { factory = (SSLSocketFactory) socketFactory; @@ -805,7 +811,7 @@ public InetSocketAddress getRemoteSocketAddress(WebSocket conn) { /** * Called when errors occurs. If an error causes the websocket connection to fail {@link - * #onClose(int, String, boolean)} will be called additionally.
This method will be called + * #onClose(int code, String, boolean)} will be called additionally.
This method will be called * primarily because of IO or protocol errors.
If the given exception is an RuntimeException * that probably means that you encountered a bug.
* @@ -857,7 +863,8 @@ private void runWriteData() throws IOException { ostream.flush(); } } catch (InterruptedException e) { - for (ByteBuffer buffer : engine.outQueue) { + for (ByteBuffer buffer = engine.outQueue.poll(); buffer != null; + buffer = engine.outQueue.poll()) { ostream.write(buffer.array(), 0, buffer.limit()); ostream.flush(); } @@ -883,17 +890,17 @@ private void closeSocket() { /** * Method to set a proxy for this connection * - * @param proxy the proxy to use for this websocket client + * @param proxy the proxy that is used for this websocket client */ public void setProxy(Proxy proxy) { if (proxy == null) { throw new IllegalArgumentException(); } - this.proxy = proxy; + this.props = proxy; } /** - * Accepts bound and unbound sockets.
This method must be called before connect. + * Accept bound and unbound sockets.
This method must be called before connect. * If the given socket is not yet bound it will be bound to the uri specified in the constructor. * * @param socket The socket which should be used for the connection @@ -908,7 +915,7 @@ public void setSocket(Socket socket) { } /** - * Accepts a SocketFactory.
This method must be called before connect. The socket + * Accept a SocketFactory.
This method must be called before connect. The socket * will be bound to the uri specified in the constructor. * * @param socketFactory The socket factory which should be used for the connection. From 18ad50c96674ec58b3227dbbcbf851418a6bd058 Mon Sep 17 00:00:00 2001 From: Capybara Date: Sat, 26 Sep 2026 11:28:11 +0800 Subject: [PATCH 2/5] Restore unrelated lines; keep only fatal Error catch (#1460) --- .../java/org/java_websocket/client/WebSocketClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/java_websocket/client/WebSocketClient.java b/src/main/java/org/java_websocket/client/WebSocketClient.java index 6ea91ba0..50423fea 100644 --- a/src/main/java/org/java_websocket/client/WebSocketClient.java +++ b/src/main/java/org/java_websocket/client/WebSocketClient.java @@ -201,6 +201,7 @@ public WebSocketClient(URI serverUri, Draft protocolDraft, Map h /** * Constructs a WebSocketClient instance and sets it to the connect to the specified URI. The * channel does not attampt to connect automatically. The connection will be established once you + * call connect. * * @param serverUri the server URI to connect to * @param protocolDraft The draft which should be used for this connection @@ -577,7 +578,7 @@ public void run() { private void upgradeSocketToSSL() throws NoSuchAlgorithmException, KeyManagementException, IOException { SSLSocketFactory factory; - // Prioritse the provided socketfactory + // Prioritise the provided socketfactory // Helps when using web debuggers like Fiddler Classic if (socketFactory instanceof SSLSocketFactory) { factory = (SSLSocketFactory) socketFactory; @@ -863,8 +864,7 @@ private void runWriteData() throws IOException { ostream.flush(); } } catch (InterruptedException e) { - for (ByteBuffer buffer = engine.outQueue.poll(); buffer != null; - buffer = engine.outQueue.poll()) { + for (ByteBuffer buffer : engine.outQueue) { ostream.write(buffer.array(), 0, buffer.limit()); ostream.flush(); } @@ -896,7 +896,7 @@ public void setProxy(Proxy proxy) { if (proxy == null) { throw new IllegalArgumentException(); } - this.props = proxy; + this.proxy = proxy; } /** From c57dc1a869e1d6b39acb165692531f3be16d5c45 Mon Sep 17 00:00:00 2001 From: Capybara Date: Sat, 26 Sep 2026 11:30:24 +0800 Subject: [PATCH 3/5] Add regression test for fatal Error in client read loop (#1460) --- .../java_websocket/issues/Issue1460Test.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/test/java/org/java_websocket/issues/Issue1460Test.java diff --git a/src/test/java/org/java_websocket/issues/Issue1460Test.java b/src/test/java/org/java_websocket/issues/Issue1460Test.java new file mode 100644 index 00000000..3c10b409 --- /dev/null +++ b/src/test/java/org/java_websocket/issues/Issue1460Test.java @@ -0,0 +1,123 @@ +package org.java_websocket.issues; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.java_websocket.WebSocket; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.drafts.Draft_6455; +import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.ServerHandshake; +import org.java_websocket.server.WebSocketServer; +import org.java_websocket.util.SocketUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression test for #1460: a fatal Error (VirtualMachineError / ThreadDeath / LinkageError) + * rethrown by WebSocketImpl#decodeFrames must reach onError and close the client connection + * instead of silently killing the connect/read thread with no callback and no cleanup. + */ +public class Issue1460Test { + + /** + * Draft that emulates a fatal Error during frame decoding, i.e. the trio + * WebSocketImpl#decodeFrames rethrows. + */ + private static class FatalErrorDraft extends Draft_6455 { + @Override + public List translateFrame(ByteBuffer buffer) { + throw new LinkageError("simulated fatal error during frame processing"); + } + } + + @Test + @Timeout(value = 10, unit = TimeUnit.SECONDS) + public void testFatalErrorTriggersOnErrorAndClose() throws InterruptedException { + int port = SocketUtil.getAvailablePort(); + final CountDownLatch startLatch = new CountDownLatch(1); + final CountDownLatch errorLatch = new CountDownLatch(1); + final CountDownLatch closeLatch = new CountDownLatch(1); + final AtomicReference capturedError = new AtomicReference<>(); + final AtomicInteger capturedCode = new AtomicInteger(-1); + + WebSocketServer server = new WebSocketServer(new InetSocketAddress(port)) { + @Override + public void onOpen(WebSocket conn, ClientHandshake handshake) { + conn.send("trigger"); + } + + @Override + public void onClose(WebSocket conn, int code, String reason, boolean remote) { + } + + @Override + public void onMessage(WebSocket conn, String message) { + } + + @Override + public void onMessage(WebSocket conn, ByteBuffer message) { + } + + @Override + public void onError(WebSocket conn, Exception ex) { + ex.printStackTrace(); + } + + @Override + public void onStart() { + startLatch.countDown(); + } + }; + + WebSocketClient client = new WebSocketClient( + URI.create("ws://localhost:" + port), new FatalErrorDraft()) { + @Override + public void onOpen(ServerHandshake handshakedata) { + } + + @Override + public void onMessage(String message) { + } + + @Override + public void onClose(int code, String reason, boolean remote) { + capturedCode.set(code); + closeLatch.countDown(); + } + + @Override + public void onError(Exception ex) { + capturedError.set(ex); + errorLatch.countDown(); + } + }; + + server.start(); + assertTrue(startLatch.await(5, TimeUnit.SECONDS), "server should start"); + client.connectBlocking(); + + try { + assertTrue(errorLatch.await(5, TimeUnit.SECONDS), + "onError must be invoked for a fatal Error"); + assertTrue(closeLatch.await(5, TimeUnit.SECONDS), + "onClose must be invoked for a fatal Error"); + assertTrue(capturedError.get().getCause() instanceof LinkageError, + "onError must wrap the fatal Error"); + assertEquals(CloseFrame.UNEXPECTED_CONDITION, capturedCode.get()); + } finally { + server.stop(); + } + } +} From 296a0f81f418a3b76fed51ce640a60e9124a9347 Mon Sep 17 00:00:00 2001 From: Capybara Date: Sat, 26 Sep 2026 11:33:37 +0800 Subject: [PATCH 4/5] Drop unrelated changes; isolate fatal Error catch (#1460) --- .../org/java_websocket/client/WebSocketClient.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/java_websocket/client/WebSocketClient.java b/src/main/java/org/java_websocket/client/WebSocketClient.java index 50423fea..fc022c95 100644 --- a/src/main/java/org/java_websocket/client/WebSocketClient.java +++ b/src/main/java/org/java_websocket/client/WebSocketClient.java @@ -446,7 +446,7 @@ public void close() { * * @throws InterruptedException Thrown when the threads get interrupted */ - public boolean closeBlocking() throws InterruptedException { + public void closeBlocking() throws InterruptedException { close(); closeLatch.await(); } @@ -812,7 +812,7 @@ public InetSocketAddress getRemoteSocketAddress(WebSocket conn) { /** * Called when errors occurs. If an error causes the websocket connection to fail {@link - * #onClose(int code, String, boolean)} will be called additionally.
This method will be called + * #onClose(int, String, boolean)} will be called additionally.
This method will be called * primarily because of IO or protocol errors.
If the given exception is an RuntimeException * that probably means that you encountered a bug.
* @@ -890,7 +890,7 @@ private void closeSocket() { /** * Method to set a proxy for this connection * - * @param proxy the proxy that is used for this websocket client + * @param proxy the proxy to use for this websocket client */ public void setProxy(Proxy proxy) { if (proxy == null) { @@ -900,7 +900,7 @@ public void setProxy(Proxy proxy) { } /** - * Accept bound and unbound sockets.
This method must be called before connect. + * Accepts bound and unbound sockets.
This method must be called before connect. * If the given socket is not yet bound it will be bound to the uri specified in the constructor. * * @param socket The socket which should be used for the connection @@ -915,7 +915,7 @@ public void setSocket(Socket socket) { } /** - * Accept a SocketFactory.
This method must be called before connect. The socket + * Accepts a SocketFactory.
This method must be called before connect. The socket * will be bound to the uri specified in the constructor. * * @param socketFactory The socket factory which should be used for the connection. From aed11609321b2398717786acd20dc3b47688cd3c Mon Sep 17 00:00:00 2001 From: Capybara Date: Sat, 26 Sep 2026 11:42:47 +0800 Subject: [PATCH 5/5] Override copyInstance so fatal Error draft survives engine copy (#1460) --- .../java_websocket/issues/Issue1460Test.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/test/java/org/java_websocket/issues/Issue1460Test.java b/src/test/java/org/java_websocket/issues/Issue1460Test.java index 3c10b409..6648f1fa 100644 --- a/src/test/java/org/java_websocket/issues/Issue1460Test.java +++ b/src/test/java/org/java_websocket/issues/Issue1460Test.java @@ -3,6 +3,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -11,7 +12,9 @@ import org.java_websocket.WebSocket; import org.java_websocket.client.WebSocketClient; +import org.java_websocket.drafts.Draft; import org.java_websocket.drafts.Draft_6455; +import org.java_websocket.extensions.IExtension; import org.java_websocket.framing.CloseFrame; import org.java_websocket.framing.Framedata; import org.java_websocket.handshake.ClientHandshake; @@ -40,6 +43,28 @@ private static class FatalErrorDraft extends Draft_6455 { public List translateFrame(ByteBuffer buffer) { throw new LinkageError("simulated fatal error during frame processing"); } + + @Override + public Draft copyInstance() { + ArrayList newExtensions = new ArrayList<>(); + for (IExtension knownExtension : getKnownExtensions()) { + newExtensions.add(knownExtension.copyInstance()); + } + ArrayList newProtocols = new ArrayList<>(); + for (org.java_websocket.protocols.IProtocol knownProtocol : getKnownProtocols()) { + newProtocols.add(knownProtocol.copyInstance()); + } + return new FatalErrorDraft(newExtensions, newProtocols, getMaxFrameSize()); + } + + FatalErrorDraft(List inputExtensions, + List inputProtocols, int inputMaxFrameSize) { + super(inputExtensions, inputProtocols, inputMaxFrameSize); + } + + FatalErrorDraft() { + super(); + } } @Test