Java WebSockets: The remote endpoint was in state [TEXT_FULL_WRITING]
I just ran into this issue today, the accepted answer wasn't the resolution for me. I tried synchronizing every call to the remote endpoint that was in my code, which was only 4 instances. That did not fix it either. I also tried updating to the latest tomcat version which at this time was 9.0.24 which did not fix it.
It seams the source of my issue is that in a single websocket message request that came in, I happen to send two DIFFERENT messages (on purpose) during the request. I verified both sendText calls were properly synchronized, they were getting called about 0.001 milliseconds a part or less in different blocks.
My solution that I worked up real quick was to use the Async version of the remote endpoint, and just make sure the last msg's future was done by the time the next msg is requested to be sent. I wasn't thrilled about this but it did fix the issue... here is the class I wrote and I just now reference this object anytime I want to send something over the websocket without requiring the code in a sync block, since the send* methods on this class are already synchronized. Hope this helps someone down the line.
NOTE: I did not synchronize anything other than the send* so not sure if Ping/Pong will have the same issue or not, I've never used those.
public class WebSocketEndpointAsync implements RemoteEndpoint.Async {
private final Session _session;
private final Async _ep;
private Future<Void> _lastFuture = null;
public WebSocketEndpointAsync(Session session, Async ep)
{
_session = session;
_ep = ep;
}
@Override public long getSendTimeout() { return _ep.getSendTimeout(); }
@Override public void setSendTimeout(long timeout) { _ep.setSendTimeout(timeout); }
@Override public void setBatchingAllowed(boolean allowed) throws IOException { _ep.setBatchingAllowed(allowed); }
@Override public boolean getBatchingAllowed() { return _ep.getBatchingAllowed(); }
@Override public void flushBatch() throws IOException { _ep.flushBatch(); }
@Override public void sendPing(ByteBuffer byteBuffer) throws IOException, IllegalArgumentException { _ep.sendPing(byteBuffer); }
@Override public void sendPong(ByteBuffer byteBuffer) throws IOException, IllegalArgumentException { _ep.sendPong(byteBuffer); }
@Override public void sendText(String s, SendHandler sendHandler) { throw new UnsupportedOperationException(); }
@Override public void sendBinary(ByteBuffer byteBuffer, SendHandler sendHandler) { throw new UnsupportedOperationException(); }
@Override public void sendObject(Object o, SendHandler sendHandler) { throw new UnsupportedOperationException(); }
protected synchronized void checkLastSendComplete() {
if (_lastFuture != null) {
try {
if (!_lastFuture.isDone()) {
// Only one write to the websocket can happen at a time, so we need to make sure the last one completed
// else we get ...
// java.lang.IllegalStateException: The remote endpoint was in state [TEXT_FULL_WRITING] which is an invalid state for called method
do { Thread.sleep(1); }
while (!_lastFuture.isDone());
}
// Get the result to ensure
var ignore = _lastFuture.get();
}
catch (InterruptedException ie) { }
catch (ExecutionException ee) { }
}
}
@Override
public synchronized Future<Void> sendText(String text) {
checkLastSendComplete();
return (_lastFuture = _ep.sendText(text));
}
@Override
public synchronized Future<Void> sendBinary(ByteBuffer byteBuffer) {
checkLastSendComplete();
return (_lastFuture = _ep.sendBinary(byteBuffer));
}
@Override
public synchronized Future<Void> sendObject(Object obj) {
checkLastSendComplete();
return (_lastFuture = _ep.sendObject(obj));
}
}
OK, this is not a Tomcat issue but my fault.
My onMessage function returned a string, what means that I echoed the message back. As result, those part of code was not synced.
Bad:
@OnMessage
public String onMessage(String message, Session session) {
...
return message;
}
Good:
@OnMessage
public void onMessage(String message, Session session) {
...
}
I found this: https://bz.apache.org/bugzilla/show_bug.cgi?id=56026
seems tomcat has done something unexpected, as a workaround you must synchronize all session.sendxxx calls no matter whether it's async.