From 4cc27db0e830b2cb509d4c98832a1d657fc48256 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Fri, 25 Sep 2026 15:12:50 +0000 Subject: [PATCH] Fail a refused connect with the server's reason An error answering the handshake, such as "too many clones already connected", used to surface only as "disconnected while waiting for CONNECTED state". connect() now fails with the server's message, and the CommandException carrying its error id is the cause. Co-Authored-By: Claude Opus 5.5 --- .../client/LocalClientHandlerConnecting.java | 18 +++++++++++++-- .../LocalClientHandlerRetrievingData.java | 9 ++++---- .../client/AbstractTeamspeakClientSocket.java | 22 +++++++++++++++++-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java index b7a30a3..ec84c5b 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerConnecting.java @@ -1,6 +1,7 @@ package com.github.manevolent.ts3j.protocol.packet.handler.client; import com.github.manevolent.ts3j.command.Command; +import com.github.manevolent.ts3j.command.CommandException; import com.github.manevolent.ts3j.command.SingleCommand; import com.github.manevolent.ts3j.command.parameter.CommandSingleParameter; import com.github.manevolent.ts3j.protocol.Packet; @@ -265,15 +266,28 @@ public class LocalClientHandlerConnecting extends LocalClientHandler { getClient().setCommandProcessor(getClient()); getClient().setState(ClientConnectionState.RETRIEVING_DATA); } else if (command.getName().equals("error")) { - getClient().setState(ClientConnectionState.DISCONNECTED); + CommandException refusal = refusal(command); + getClient().refuse(refusal); - throw new IOException(command.get("msg").getValue()); + throw new IOException(refusal.getMessage(), refusal); } else { throw new IOException("Unknown Init command: " + command.getName()); } } } + /** The server's answer to a handshake command it refused, as an exception carrying its error id. */ + static CommandException refusal(SingleCommand error) { + String message = error.has("msg") ? error.get("msg").getValue() : "connection refused"; + int id; + try { + id = error.has("id") ? Integer.parseInt(error.get("id").getValue()) : -1; + } catch (NumberFormatException e) { + id = -1; + } + return new CommandException(message, id); + } + private SingleCommand createInitIv() { alphaBytes = new byte[10]; new Random().nextBytes(alphaBytes); diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java index 77bbf49..87a7021 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/packet/handler/client/LocalClientHandlerRetrievingData.java @@ -1,9 +1,9 @@ package com.github.manevolent.ts3j.protocol.packet.handler.client; +import com.github.manevolent.ts3j.command.CommandException; import com.github.manevolent.ts3j.command.CommandProcessException; import com.github.manevolent.ts3j.command.CommandProcessor; import com.github.manevolent.ts3j.command.MultiCommand; -import com.github.manevolent.ts3j.protocol.client.ClientConnectionState; import com.github.manevolent.ts3j.protocol.socket.client.LocalTeamspeakClientSocket; @@ -15,13 +15,14 @@ public class LocalClientHandlerRetrievingData extends LocalClientHandlerFull { @Override protected void handleCommand(MultiCommand command) throws CommandProcessException { if (command.getName().equalsIgnoreCase("error")) { + // clientinit was refused, e.g. for a nickname in use or a wrong server password. + CommandException refusal = LocalClientHandlerConnecting.refusal(command.simplifyOne()); try { - getClient().setState(ClientConnectionState.DISCONNECTED); + getClient().refuse(refusal); } catch (Exception e) { getClient().getExceptionHandler().accept(e); } - - throw new CommandProcessException(command.simplifyOne().get("msg").getValue()); + throw new CommandProcessException(refusal.getMessage()); } CommandProcessor processor = getClient().getCommandProcessor(); diff --git a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java index 7573ce7..35d9fb7 100644 --- a/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java +++ b/src/main/java/com/github/manevolent/ts3j/protocol/socket/client/AbstractTeamspeakClientSocket.java @@ -1,5 +1,7 @@ package com.github.manevolent.ts3j.protocol.socket.client; +import com.github.manevolent.ts3j.command.CommandException; + import com.github.manevolent.ts3j.command.*; import com.github.manevolent.ts3j.identity.Identity; import com.github.manevolent.ts3j.protocol.NetworkPacket; @@ -52,6 +54,8 @@ public abstract class AbstractTeamspeakClientSocket private final Map clientOptions = new ConcurrentHashMap<>(); private final Object connectionStateLock = new Object(); + /** Why the server turned the connection down while it was being set up; null otherwise. */ + private volatile CommandException refusal; private final LinkedBlockingDeque readQueue = new LinkedBlockingDeque<>(65536); @@ -172,8 +176,10 @@ public abstract class AbstractTeamspeakClientSocket boolean initialState = this.connectionState == null; - if (state == ClientConnectionState.CONNECTING) + if (state == ClientConnectionState.CONNECTING) { + refusal = null; setReading(true); + } this.connectionState = state; @@ -260,13 +266,25 @@ public abstract class AbstractTeamspeakClientSocket connectionStateLock.wait(Math.max(0L, wait - (System.currentTimeMillis() - start))); } - if (connectionState != state && connectionState == ClientConnectionState.DISCONNECTED) + if (connectionState != state && connectionState == ClientConnectionState.DISCONNECTED) { + CommandException refused = refusal; + if (refused != null) throw new IOException(refused.getMessage(), refused); throw new IOException("disconnected while waiting for " + state.name() + " state"); + } if (connectionState != state) throw new TimeoutException("timeout waiting for " + state.name() + " state"); } } + /** + * Records the error the server answered a handshake command with, and leaves the connection: + * {@link #waitForState} then fails with it rather than with a bare disconnect. + */ + public void refuse(CommandException error) throws IOException, TimeoutException { + refusal = error; + setState(ClientConnectionState.DISCONNECTED); + } + public boolean isConnected() { return connectionState == ClientConnectionState.CONNECTED; }