I have a standard websocket endpoint based on Tyrus implementation which times to times triggers the java.lang.IllegalStateException: Cannot set WriteListener for non-async or non-upgrade request
. We are running on Payara 4.1.
My standard implementation
@ServerEndpoint(value = "...", decoders=MessageDecoder.class, encoders=MessageEncoder.class)
public class EndpointImpl extends AbstractEndpoint{
// onOpen, onClose, onMessage, onError methods
}
Where the abstract class is
public abstract class AbstractEndpoint{
// irrelevant onOpen, onOpen handling method
117 protected void sendMessage(Session session, Message message){
118 if(message == null){
119 LOGGER.error("null message");
120 } else if(!session.isOpen()){
121 LOGGER.error("session is not opened");
122 } else{
>>>123 session.getAsyncRemote().sendObject(message, (result) -> {
124 if (result.isOK()) {
125 LOGGER.info("success! yeah!");
126 } else {
127 LOGGER.error("error when sending message", result.getException());
128 }
129 });
130 }
}
}
IllegalStateException
So far, nothing special. I can perfectly communicate and respond to the request I received and, websocket FTW, I can push information and get back the feedback. However, I times to times receive an exception:
java.lang.IllegalStateException: Cannot set WriteListener for non-async or non-upgrade request
at org.apache.catalina.connector.OutputBuffer.setWriteListener(OutputBuffer.java:536)
at org.apache.catalina.connector.CoyoteOutputStream.setWriteListener(CoyoteOutputStream.java:223)
at org.glassfish.tyrus.servlet.TyrusServletWriter.write(TyrusServletWriter.java:140)
at org.glassfish.tyrus.core.ProtocolHandler.write(ProtocolHandler.java:486)
at org.glassfish.tyrus.core.ProtocolHandler.send(ProtocolHandler.java:274)
at org.glassfish.tyrus.core.ProtocolHandler.send(ProtocolHandler.java:332)
at org.glassfish.tyrus.core.TyrusWebSocket.sendText(TyrusWebSocket.java:317)
at org.glassfish.tyrus.core.TyrusRemoteEndpoint.sendSyncObject(TyrusRemoteEndpoint.java:429)
at org.glassfish.tyrus.core.TyrusRemoteEndpoint$Async.sendAsync(TyrusRemoteEndpoint.java:352)
at org.glassfish.tyrus.core.TyrusRemoteEndpoint$Async.sendObject(TyrusRemoteEndpoint.java:249)
at com.mycompany.websocket.AbstEndpoint.sendMessage(AbstEndpoint.java:123)
Second sendMessage method attempt
At first, I thought that my asynchronous endpoint was wrongly configured so I tried the Future<> way instead of the callback way:
RemoteEndpoint.Async async = session.getAsyncRemote();
async.setSendTimeout(5000); // 5 seconds
Future<Void> future = async.sendObject(message);
try{
future.get();
}
catch(InterruptedException | ExecutionException ex){
LOGGER.error("error when sending message", ex);
}
I also got the exception.
So far and symptoms
Surprisingly, I only found one link talking about this issue.
- The github link highlights a buffer size issue. I don't use partial messages, only whole messages. Moreover, regardless if I'm using the default buffer size or I set a new one, the exception comes
- I could not find a global rule about how to reproduce the error
- After the exception was raised, the client could keep sending messages and the server would process it but the server never replied to the client. It appears that the outgoing communication channel is blocked
- As the server keeps processing incoming messages, the websocket channel is not closed after the exception
Digging in Tyrus implementation
I browsed the tyrus-core implementation to found out that the sending method is depending on some Grizzly component. I don't know anything about Grizzly but it appears that the sending must be synchronous anyway due to some Grizzly restriction
Questions
- Did someone already meet such a situation? If yes, does the exception really means that there is a bottleneck somewhere or it means something else?
- Is tyrus asynchronous endpoint really asynchronous, ie like "process-and-forget"?
- I haven't found any way to have and outgoing messages queued: if a message A is long, wait for message A sending to finish before sending message B. Is there a way to handle large messages in websocket or the asynchronous endpoint is the only way?
- I want to make sure that the sending did not encounter any issue, hence my choice of a asynchronous solution. Should I go back to a synchronous way?
I haven't detailed my Tyrus investigation. If you feel it relevant, feel free to ask and I'll gladly develop.
In order for a request to be fully asynchronous, any
Filter
in the request-response chain must explicitly be set to support asynchronous requests. Specifically those "catch-all" filters which are mapped on/*
.In case the filter is registered via
<filter>
entry inweb.xml
, this can be done by setting child element<async-supported>
totrue
.In case the filter is registered via
@WebFilter
annotation, this can be done by setting itsasyncSupported
attribute totrue
.In case the filter is registered via
ServletContext#addFilter()
, this can be done by settingRegistration.Dynamic#setAsyncSupported()
. totrue
.Reason is, the WebSocket implementation internally uses
ServletRequest#startAsync()
during the handshake request in order to keep the request-response pipeline "forever" open until the response is explicitly closed. Its javadoc says the following:The
isAsyncSupported()
defaults tofalse
in order to not break existing web applications using poorly implemented servlet filters. Technically, it should suffice to mark only the targetServlet
as async supported and leave the filters alone. A sane "catch-all"Filter
wouldn't explicitly write anything to the HTTP response, but the Servlet API did never forbid that and thus such filters can unfortunately exist.In case you have one such filter, then you should either fix it to not write anything to the response anymore so that you can safely mark it to support async requests, or to adjust its URL pattern to not cover WebSocket requests. I.e. don't map it on
/*
anymore.