Tyrus websocket: IllegalStateException cannot set

2019-01-15 22:08发布

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.

  1. 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
  2. I could not find a global rule about how to reproduce the error
  3. 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
  4. 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

  1. 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?
  2. Is tyrus asynchronous endpoint really asynchronous, ie like "process-and-forget"?
  3. 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?
  4. 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.

1条回答
闹够了就滚
2楼-- · 2019-01-15 22:52

java.lang.IllegalStateException: Cannot set WriteListener for non-async or non-upgrade request

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 in web.xml, this can be done by setting child element <async-supported> to true.

<filter>
    ...
    <async-supported>true</async-supported>
</filter>

In case the filter is registered via @WebFilter annotation, this can be done by setting its asyncSupported attribute to true.

@WebFilter(..., asyncSupported="true")

In case the filter is registered via ServletContext#addFilter(), this can be done by setting Registration.Dynamic#setAsyncSupported(). to true.

Dynamic filter = servletContext.addFilter(name, type);
filter.setAsyncSupported(true);

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:

Throws
IllegalStateException - if this request is within the scope of a filter or servlet that does not support asynchronous operations (that is, isAsyncSupported() returns false), or if this method is called again without any asynchronous dispatch (resulting from one of the AsyncContext.dispatch() methods), is called outside the scope of any such dispatch, or is called again within the scope of the same dispatch, or if the response has already been closed

The isAsyncSupported() defaults to false in order to not break existing web applications using poorly implemented servlet filters. Technically, it should suffice to mark only the target Servlet 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.

查看更多
登录 后发表回答