springspring-bootstompspring-framework-beans

Spring STOMP Incomplete Frame


I have created a websocket using STOMP inside Spring. The endpoint works like a charm when used with javascript libraries however when I use any of the simple websocket google chrome extensions (i.e. Simple WebSocket Client, Smart Websocket Client, Web Socket Client), spring throws the "Incomplete STOMP frame content message. Diving into the code, I've been able to see that the cause of this is I cannot insert the null character /u0000 with any of these tools. I assume all the java script frameworks do this by default. Has someone found a workaround for this so that I can use any websocket client with Spring STOMP?

The stomp code is located here: https://github.com/spring-projects/spring-framework/blob/master/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java

On [currently] lines 308-320 the below code exists. This method returns null because byteBuffer.remaining is not greater than content length (both are 0) .There is a StompSubProtocolHandler exception that fires afterrwards. I tried looking into all the handlers and interceptors but there doesn't seem to be a way to intercept things at this level without rewriting almost everything. I wanted to just inject "\0" into the payload...

if (contentLength != null && contentLength >= 0) {
        if (byteBuffer.remaining() > contentLength) {
            byte[] payload = new byte[contentLength];
            byteBuffer.get(payload);
            if (byteBuffer.get() != 0) {
                throw new StompConversionException("Frame must be terminated with a null octet");
            }
            return payload;
        }
        else {
            return null;
        }
    }

Solution

  • I had exactly the same problem, I tested with Web socket client.

    In order to be able to test STOMP manually on my local environment, I have configured my Spring context. That way I don't need to add the null character on the client side. It's automatically added if it does not exist.

    For that, in the class AbstractWebSocketMessageBrokerConfigurer I added:

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
            @Override
            public WebSocketHandler decorate(WebSocketHandler webSocketHandler) {
                return new EmaWebSocketHandlerDecorator(webSocketHandler);
            }
        });
    }
    

    The decorator add automatically carriage returns when there is no request body (ex: connect command).

    /**
     * Extension of the {@link WebSocketHandlerDecorator websocket handler decorator} that allows to manually test the
     * STOMP protocol.
     *
     * @author Sebastien Gerard
     */
    public class EmaWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
    
        private static final Logger logger = LoggerFactory.getLogger(EmaWebSocketHandlerDecorator.class);
    
        public EmaWebSocketHandlerDecorator(WebSocketHandler webSocketHandler) {
            super(webSocketHandler);
        }
    
        @Override
        public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
            super.handleMessage(session, updateBodyIfNeeded(message));
        }
    
        /**
         * Updates the content of the specified message. The message is updated only if it is
         * a {@link TextMessage text message} and if does not contain the <tt>null</tt> character at the end. If
         * carriage returns are missing (when the command does not need a body) there are also added.
         */
        private WebSocketMessage<?> updateBodyIfNeeded(WebSocketMessage<?> message) {
            if (!(message instanceof TextMessage) || ((TextMessage) message).getPayload().endsWith("\u0000")) {
                return message;
            }
    
            String payload = ((TextMessage) message).getPayload();
    
            final Optional<StompCommand> stompCommand = getStompCommand(payload);
    
            if (!stompCommand.isPresent()) {
                return message;
            }
    
            if (!stompCommand.get().isBodyAllowed() && !payload.endsWith("\n\n")) {
                if (payload.endsWith("\n")) {
                    payload += "\n";
                } else {
                    payload += "\n\n";
                }
            }
    
            payload += "\u0000";
    
            return new TextMessage(payload);
        }
    
        /**
         * Returns the {@link StompCommand STOMP command} associated to the specified payload.
         */
        private Optional<StompCommand> getStompCommand(String payload) {
            final int firstCarriageReturn = payload.indexOf('\n');
    
            if (firstCarriageReturn < 0) {
                return Optional.empty();
            }
    
            try {
                return Optional.of(
                        StompCommand.valueOf(payload.substring(0, firstCarriageReturn))
                );
            } catch (IllegalArgumentException e) {
                logger.trace("Error while parsing STOMP command.", e);
    
                return Optional.empty();
            }
        }
    }
    

    Now I can do requests like:

    CONNECT
    accept-version:1.2
    host:localhost
    content-length:0
    
    
    SEND
    destination:/queue/com.X.notification-subscription
    content-type:text/plain
    reply-to:/temp-queue/notification
    
    hello world :)
    

    Hope this helps.

    S.