Search code examples
spring-bootwebsocketspring-websocketstompstompjs

STOMP over WebSockets: Spring Boot expects JSON; NodeJs STOMP.js client fails to connect


When trying out STOMP over WebSockets, I noticed inconsistencies between different implementations, namely between a Spring Boot Java implementation and a NodeJs client written with STOMP.js.

When debugging into it, the difference is that in the Spring Boot app, the CONNECT message is expected to be a JSON array. For instance, this message is sent by their test client (written in JavaScript using the SocksJS library):

["CONNECT\naccept-version:1.1,1.0\nheart-beat:10000,10000\n\n\u0000"]

In contrast, my NodeJs STOMP.js test client (code is below) sends the following frame:

CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000

^@

Unfortunately, I am not experienced with STOMP, but after reading through the specification, I did not understand why Spring Boot expects the data to be represented as a JSON array. Is this a known problem?


To demonstrate, let me share two example runs. One successful run to connect to RabbitMQ, followed by a failed attempt to connect against the Java Spring Boot app. (A reproducible setup with the code can be found at the end.)

  1. Connect to RabbitMQ instance, which is configure to use STOMP over WebSockets (running on ws://localhost:15674/ws):
$ node client.js 
Opening Web Socket...
Web Socket Opened...
>>> CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000


Received data
<<< CONNECTED
server:RabbitMQ/3.8.8
session:session-WkKD6rN5BNc_ObKpziikYA
heart-beat:4000,4000
version:1.2



connected to server RabbitMQ/3.8.8
send PING every 4000ms
check PONG every 4000ms
onConnect called
<<< PONG
Received data
<<< 

<<< PONG
>>> PING
Received data
<<< 
  1. Now connect (unsuccessfully) against the Spring Boot app (ws://localhost:5555/chat/123/k2qn3dl7/websocket):
node client.js 
Opening Web Socket...
Web Socket Opened...
>>> CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000


Received data
<<< o
Received data
<<< c[1007,""]
Connection closed to ws://localhost:5555/chat/123/k2qn3dl7/websocket
STOMP: scheduling reconnection in 5000ms
Opening Web Socket...
Web Socket Opened...
>>> CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000


Received data
<<< o
^C

The reason why it fails is that Jackson (the JSON parser) failed to parse that payload:

CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000

^@

As said, in the client that comes with the Spring Boot example, the payload looked like that:

["CONNECT\naccept-version:1.1,1.0\nheart-beat:10000,10000\n\n\u0000"]

Here is the full error in the Spring Boot app:

2021-07-22 13:58:59.546  INFO 74313 --- [nio-5555-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2021-07-22 13:58:59.594 ERROR 74313 --- [nio-5555-exec-1] s.w.s.s.t.s.WebSocketServerSockJsSession : Broken data received. Terminating WebSocket connection abruptly

com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'CONNECT': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (String)"CONNECT
accept-version:1.0,1.1,1.2
heart-beat:4000,4000

"; line: 1, column: 8]
    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2337) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:720) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._reportInvalidToken(ReaderBasedJsonParser.java:2903) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.core.json.ReaderBasedJsonParser._handleOddValue(ReaderBasedJsonParser.java:1949) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.core.json.ReaderBasedJsonParser.nextToken(ReaderBasedJsonParser.java:781) ~[jackson-core-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4684) ~[jackson-databind-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4586) ~[jackson-databind-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548) ~[jackson-databind-2.12.3.jar:2.12.3]
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516) ~[jackson-databind-2.12.3.jar:2.12.3]
    at org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec.decode(Jackson2SockJsMessageCodec.java:64) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession.handleMessage(WebSocketServerSockJsSession.java:187) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler.handleTextMessage(SockJsWebSocketHandler.java:93) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.handler.AbstractWebSocketHandler.handleMessage(AbstractWebSocketHandler.java:43) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:114) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.java:43) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:85) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:82) ~[spring-websocket-5.3.8.jar:5.3.8]
    at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:415) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:129) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:515) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:301) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:85) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:183) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:162) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:156) ~[tomcat-embed-websocket-9.0.46.jar:9.0.46]
    at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:60) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:59) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.46.jar:9.0.46]
    at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]

2021-07-22 13:59:04.610 ERROR 74313 --- [nio-5555-exec-2] s.w.s.s.t.s.WebSocketServerSockJsSession : Broken data received. Terminating WebSocket connection abruptly

Path to reproduce:

  1. NodeJs client code
  2. Spring Boot test app
  3. RabbitMQ test instance

  1. Client code written in NodeJs:
// Required dependencies:
// "@stomp/stompjs": "6.1.0"
// "websocket": "1.0.34"

// Polyfills. For details see:
// https://stomp-js.github.io/guide/stompjs/rx-stomp/ng2-stompjs/pollyfils-for-stompjs-v5.html
Object.assign(global, { WebSocket: require('websocket').w3cwebsocket });

const StompJs = require('@stomp/stompjs');

const client = new StompJs.Client({
        //brokerURL: 'ws://localhost:15674/ws', // RabbitMQ (should work)
        brokerURL: 'ws://localhost:5555/chat/123/k2qn3dl7/websocket', // Spring app (should fail)
        reconnectDelay: 5000,
        heartbeatIncoming: 4000,
        heartbeatOutgoing: 4000,
        logRawCommunication: true,
        debug: (x) => console.log(x),
});

client.onConnect = function (frame) {
  console.log('onConnect called');
};

client.activate();

  1. The Spring Boot app can be found here. I started it on port 5555:
git clone [email protected]:eugenp/tutorials.git
cd tutorials/spring-websockets
SERVER_PORT=5555 mvn spring-boot:run

Note: if you then go to http://localhost:5555, you will see a chat application served by the Spring Boot app. When you click connect, a STOMP connection will be established.

Firefox network monitor


  1. To start RabbitMQ, you can use the Docker container used for the tests in STOMP.js:
git clone [email protected]:stomp-js/stompjs.git
cd stompjs
sudo docker build -t myrabbitmq rabbitmq/
sudo docker run --rm -p 15674:15674 myrabbitmq

Solution

  • In short: The JSON messages were not "STOMP over native WebSockets" but "STOMP over SocksJS". The additional JSON layer was introduced by the SocksJS protocol, which is used in the Spring Boot example application.


    Here is the longer story. It turned out, that my endpoint was wrong. Instead of

    'ws://localhost:5555/chat/123/k2qn3dl7/websocket'
    

    it should have been

    'ws://localhost:5555/chat'
    

    It had the wrong URI because I was copying the output that I saw in the browser. Instead I should have looked at the configuration:

    @Override
    public void registerStompEndpoints(final StompEndpointRegistry registry) {
      registry.addEndpoint("/chat");
      registry.addEndpoint("/chat").withSockJS();
      registry.addEndpoint("/chatwithbots");
      registry.addEndpoint("/chatwithbots").withSockJS();
    }
    

    Now the confusing part. As can be seen from the configuration, the Spring Boot application defines fallbacks with SocksJS.

    If you remove the fallback, the confusing error message goes away. Yet when the fallback is active, Spring will try to process the request as SocksJS. That is why it tries to parse the STOMP frame as JSON, which results in the misleading error message.

    In addition, I got confused by the JavaScript client used in the Spring Boot example:

    function connect() {
      var socket = new SockJS('/chat');
      stompClient = Stomp.over(socket);
    
      stompClient.connect({}, function(frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/messages', function(messageOutput) {
          showMessageOutput(JSON.parse(messageOutput.body));
        });
      });
    }
    

    It is not connected over native WebSocket but over SocksJS. That explains why Firefox shows JSON requests, not the expected STOMP frames.