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 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 15:12:50 +00:00
co-authored by Claude Opus 5.5
parent d89a02957a
commit 4cc27db0e8
3 changed files with 41 additions and 8 deletions
@@ -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);
@@ -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();
@@ -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<String, Object> 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<Packet> 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;
}