I am creating a prototype of social network application with chat and friendship management.
When user logs in, he creates a STOMP ws connection and subscribes to broker for status (online/offline) of his friends. When the connection is created, he is saved into server memory as an online user. What I want to happen is at the moment he subscribes, he receives a list of online friends as a response and at the same time, all his online friends receive message on the same subscription destination that he is online.
So far, my code looks like this:
WebSocketConfiguration
@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
@RequiredArgsConstructor
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/specific-user", "/queue/friend-status");
registry.setApplicationDestinationPrefixes("/messaging");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/messaging/chat")
.setAllowedOriginPatterns("http://localhost:3000", "client:3000");
registry.addEndpoint("/messaging/status-change")
.setAllowedOriginPatterns("http://localhost:3000", "client:3000");
}
WebSocketController
@Controller
@RequiredArgsConstructor
public class WebSocketController {
private final IUserStatusDispatcher userStatusDispatcher;
@SubscribeMapping("/user/queue/friend-status")
public List<FriendStatusChangedMessage> subscribeToFriendStatuses(Principal user) {
return userStatusDispatcher.distributeOnlineFriendsStatusMessage(user.getName());
}
}
What UserStatusDispatcher
does is it listens for CONNECT
and DISCONNECT
events on which:
- CONNECT - saves user's name and login into memory
- DISCONNECT - sends message to all online friends that he went offline
When calling distributeOnlineFriendsStatusMessage(user.getName());
it does basically the same for ONLINE status - sends message to all his friends that he went online, PLUS returns a list of messages for new subscriber about all of his online friends. It all boils down into this piece of code:
UserStatusDispatcher
private void sendUserStatusChangeMessageToFriends(final FriendStatusChangedMessage message, final List<IUserSearchResultDO> onlineFriends) {
final List<FriendStatusChangedMessage> newUserOnlineMessageList = List.of(message);
for (final var onlineFriend : onlineFriends) {
simpleMessagingTemplate.convertAndSend("/user/" + onlineFriend.getLogin() + "/queue/friend-status", newUserOnlineMessageList);
}
}
This is my code on the client side (REACT with raw STOMP):
const [name, login] = useUserData();
const redirecting = useUnauthRedirect('/login');
const authHeader = useAuthHeader();
useEffect(() => {
if (redirecting || !authHeader) {
return () => {};
}
const onConnect = () => {
client.subscribe('/messaging/user/queue/friend-status', message => console.log("message:", message), { 'Authorization': authHeader.headers.Authorization});
}
let client = new Client({
brokerURL: 'ws://localhost:8080/messaging/status-change',
connectHeaders: { 'Authorization': authHeader.headers.Authorization},
reconnectDelay: 5000,
onConnect: onConnect,
onDisconnect: () => console.log("disconnected"),
beforeConnect: () => { }
});
client.activate();
return () => client.deactivate();
}, [redirecting]);
But here comes the issue - the returned message from @SubscribeMapping
method arrives just fine and I see it's log in the console. But the other messages, sent to user's friends never arrive. I've tried removing the /user
prefix on all possible places, tried different combinations but nothing seems to work.
I also tried switching convertAndSend
for convertAndSendToUser
but the behavior is exactly the same.
This is the server log I get when connecting and subscribing:
2022-01-07 12:34:49.298 DEBUG 119557 --- [nio-8080-exec-7] o.s.w.s.s.s.WebSocketHttpRequestHandler : GET /messaging/status-change
2022-01-07 12:34:49.301 DEBUG 119557 --- [nio-8080-exec-7] o.s.web.servlet.DispatcherServlet : Completed 101 SWITCHING_PROTOCOLS
2022-01-07 12:34:49.303 DEBUG 119557 --- [nio-8080-exec-7] s.w.s.h.LoggingWebSocketHandlerDecorator : New StandardWebSocketSession[id=548bf16b-647c-389f-d81a-522a86f77812, uri=ws://localhost:8080/messaging/status-change]
2022-01-07 12:34:49.340 TRACE 119557 --- [nio-8080-exec-8] o.s.messaging.simp.stomp.StompDecoder : Decoded CONNECT {Authorization=[Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtYWthcmVrQHBpYS5jeiIsImV4cCI6MTY0MTU1OTUzOCwiaWF0IjoxNjQxNTQxNTM4fQ.iEkhm7esbgxpAaK0eIygT31ikh5cLJtqq88o43m7qer5nEHPFBpk5-fmhHLyBHD-PWVPzjfH5dZIKw8S7yj63A], accept-version=[1.0,1.1,1.2], heart-beat=[10000,10000]} session=null
2022-01-07 12:34:49.342 DEBUG 119557 --- [nio-8080-exec-8] c.m.f.a.s.impl.UserAuthServiceImpl : Start of loadUserByUsername method (args: makarek@pia.cz).
2022-01-07 12:34:49.344 DEBUG 119557 --- [nio-8080-exec-8] org.hibernate.SQL : select userdo0_.id as id1_1_0_, userroledo2_.id as id1_0_1_, userdo0_.login as login2_1_0_, userdo0_.name as name3_1_0_, userdo0_.password as password4_1_0_, userroledo2_.name as name2_0_1_, roles1_.id_user as id_user1_2_0__, roles1_.id_role as id_role2_2_0__ from auth_user userdo0_ inner join auth_user_role roles1_ on userdo0_.id=roles1_.id_user inner join auth_role userroledo2_ on roles1_.id_role=userroledo2_.id where userdo0_.login=?
2022-01-07 12:34:49.347 DEBUG 119557 --- [nio-8080-exec-8] c.m.f.a.s.impl.UserAuthServiceImpl : End of loadUserByUsername method.
2022-01-07 12:34:49.426 DEBUG 119557 --- [boundChannel-17] o.s.m.s.b.SimpleBrokerMessageHandler : Processing CONNECT user=makarek@pia.cz session=548bf16b-647c-389f-d81a-522a86f77812
2022-01-07 12:34:49.499 DEBUG 119557 --- [tboundChannel-6] org.hibernate.SQL : select userdo0_.name as col_0_0_ from auth_user userdo0_ where userdo0_.login=?
2022-01-07 12:34:49.502 TRACE 119557 --- [tboundChannel-6] o.s.messaging.simp.stomp.StompEncoder : Encoding STOMP CONNECTED, headers={version=[1.2], heart-beat=[0,0], user-name=[makarek@pia.cz]}
2022-01-07 12:34:49.542 TRACE 119557 --- [nio-8080-exec-9] o.s.messaging.simp.stomp.StompDecoder : Decoded SUBSCRIBE {Authorization=[Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtYWthcmVrQHBpYS5jeiIsImV4cCI6MTY0MTU1OTUzOCwiaWF0IjoxNjQxNTQxNTM4fQ.iEkhm7esbgxpAaK0eIygT31ikh5cLJtqq88o43m7qer5nEHPFBpk5-fmhHLyBHD-PWVPzjfH5dZIKw8S7yj63A], id=[sub-0], destination=[/messaging/user/queue/friend-status]} session=null
2022-01-07 12:34:49.543 DEBUG 119557 --- [boundChannel-19] .WebSocketAnnotationMethodMessageHandler : Searching methods to handle SUBSCRIBE /messaging/user/queue/friend-status id=sub-0 session=548bf16b-647c-389f-d81a-522a86f77812, lookupDestination='/user/queue/friend-status'
2022-01-07 12:34:49.543 DEBUG 119557 --- [boundChannel-19] .WebSocketAnnotationMethodMessageHandler : Invoking WebSocketController#subscribeToFriendStatuses[1 args]
2022-01-07 12:34:49.848 TRACE 119557 --- [boundChannel-19] o.s.messaging.handler.HandlerMethod : Arguments: [UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=makarek@pia.cz, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_USER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]]
2022-01-07 12:34:49.850 DEBUG 119557 --- [boundChannel-19] org.hibernate.SQL : SELECT au.login as login, au.name as name FROM auth_user au INNER JOIN user_relationship ur on au.id IN (ur.id_sender, ur.id_receiver) AND (SELECT id from auth_user WHERE login = ?) IN (ur.id_sender, ur.id_receiver) AND au.login != ? INNER JOIN relationship_status rs ON rs.id = ur.id_status WHERE rs.name = 'FRIENDS' AND au.login IN (?, ?)
2022-01-07 12:34:50.527 TRACE 119557 --- [boundChannel-19] o.s.m.s.u.UserDestinationMessageHandler : Translated /user/admin@admin.com/queue/friend-status -> [/queue/friend-status-user6b803ac4-14c3-e393-27c4-4e1325d49c78]
2022-01-07 12:34:50.693 DEBUG 119557 --- [boundChannel-19] o.s.m.s.b.SimpleBrokerMessageHandler : Processing MESSAGE destination=/queue/friend-status-user6b803ac4-14c3-e393-27c4-4e1325d49c78 session=null payload=[{"username":"makarek@pia.cz","name":"Makarek","status":"ONLINE"}]
2022-01-07 12:34:50.876 TRACE 119557 --- [boundChannel-19] HandlerMethodReturnValueHandlerComposite : Processing return value with org.springframework.messaging.simp.annotation.support.SubscriptionMethodReturnValueHandler@5ffec4d5
2022-01-07 12:34:50.876 DEBUG 119557 --- [boundChannel-19] a.s.SubscriptionMethodReturnValueHandler : Reply to @SubscribeMapping: [FriendStatusChangedMessage[username=admin@admin.com, name=Admin, status=ONLINE]]
2022-01-07 12:34:50.877 TRACE 119557 --- [tboundChannel-7] o.s.messaging.simp.stomp.StompEncoder : Encoding STOMP MESSAGE, headers={destination=[/messaging/user/queue/friend-status], content-type=[application/json], subscription=[sub-0], message-id=[548bf16b-647c-389f-d81a-522a86f77812-2]}
It is visible in the log, that response is returned and even the MESSAGE
type message is parsed and sent onto right url. But on client side the message never arrives.
I've visited many questions about similar problem, where many people suggested solutions but nothing has helped so far. (Spring boot, WebSocket doesn't send notification to the specified user, Does Spring @SubscribeMapping really subscribe the client to some topic?, Sending message to specific user on Spring Websocket)