javaactivemq-classicstompspring-websocketspring-messaging

How to send ERROR message to STOMP clients with Spring WebSocket?


I am using Spring's STOMP over WebSocket implementation with a full-featured ActiveMQ broker. When users SUBSCRIBE to a topic, there is some permissions logic that they must pass through before being successfully subscribed. I am using a ChannelInterceptor to apply the permissions logic, as configured below:

WebSocketConfig.java:

@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/stomp")
      .setAllowedOrigins("*")
      .withSockJS();
  }

  @Override
  public void configureMessageBroker(MessageBrokerRegistry registry) {
    registry.enableStompBrokerRelay("/topic", "/queue")
      .setRelayHost("relayhost.mydomain.com")
      .setRelayPort(61613);
  }

  @Override
  public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.setInterceptors(new MySubscriptionInterceptor());
  }


}

WebSocketSecurityConfig.java:

public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

  @Override
  protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
    messages
      .simpSubscribeDestMatchers("/stomp/**").authenticated()
      .simpSubscribeDestMatchers("/user/queue/errors").authenticated()
      .anyMessage().denyAll();
  }

}

MySubscriptionInterceptor.java:

public class MySubscriptionInterceptor extends ChannelInterceptorAdapter {

  @Override
  public Message<?> preSend(Message<?> message, MessageChannel channel) {

    StompHeaderAccessor headerAccessor= StompHeaderAccessor.wrap(message);
    Principal principal = headerAccessor.getUser();

    if (StompCommand.SUBSCRIBE.equals(headerAccessor.getCommand())) {
      checkPermissions(principal);
    }

    return message;
  }

  private void checkPermissions(Principal principal) {
    // apply permissions logic
    // throw Exception permissions not sufficient
  }
}

When clients who do not have adequate permissions attempt to subscribe to a restricted topic, they never actually receive any messages from the topic BUT are also not notified of the exception that was thrown which rejected their subscription. Instead, the client is handed back a dead subscription that the ActiveMQ broker knows nothing about. (Normal, adequately-permissioned client interactions with the STOMP endpoint and topics work just as expected.)

I have tried subscribing to users/{subscribingUsername}/queue/errors and just plain users/queue/errors with my Java test client after it is successfully connected, but I have thus far been unable to get sort of error message about the subscription exception from the server delivered to the client. This is obviously less than ideal since clients are never notified that they've been denied access.


Solution

  • You can't just throw exception from the MySubscriptionInterceptor on the clientInboundChannel, because the last one is ExecutorSubscribableChannel, therefore is async and any exceptions from those threads are end up in the logs with any re-throw to the caller - StompSubProtocolHandler.handleMessageFromClient.

    But what you can do there is something like clientOutboundChannel and use it like this:

    StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.ERROR);
    headerAccessor.setMessage(error.getMessage());
    
    clientOutboundChannel.send(MessageBuilder.createMessage(new byte[0], headerAccessor.getMessageHeaders()));
    

    Another option to consider is Annotation mapping:

        @SubscribeMapping("/foo")
        public void handleWithError() {
            throw new IllegalArgumentException("Bad input");
        }
    
        @MessageExceptionHandler
        @SendToUser("/queue/error")
        public String handleException(IllegalArgumentException ex) {
            return "Got error: " + ex.getMessage();
        }