Description
May be realted to #27503.
I am fiddeling around with a VueJs application maintaining a WS connection to a Spring Boot backend.
This is my WebsocketConfig
@Log4j2 @Configuration @EnableWebSocketMessageBroker @Order(Ordered.HIGHEST_PRECEDENCE + 99) // custom authentication interceptor must have the highest precedence over Spring Security filters public class WebsocketConfig implements WebSocketMessageBrokerConfigurer { /** * The base path for all endpoints where Websocket/ STOMP messages can arrive. */ public static final String WS_MESSAGE_ENDPOINT_BASE_PATH = "/api/socket"; /** * The base path for any subscribable Websocket topics. */ public static final String WS_SUBSCRIPTION_BASE_PATH = "/api/socket/topics"; /** * The decoder for JWTs, Bean from Spring's Security config. */ private final JwtDecoder jwtDecoder; private final AuthConverterConfig.Jwt2AuthenticationConverter authenticationConverter; @Autowired public WebsocketConfig( JwtDecoder jwtDecoder, AuthConverterConfig.Jwt2AuthenticationConverter authenticationConverter ) { this.jwtDecoder = jwtDecoder; this.authenticationConverter = authenticationConverter; } @Bean @ConditionalOnProperty(value = "core.ignore-ws-settings", havingValue = "false", matchIfMissing = true) public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxTextMessageBufferSize(2048 * 2048); container.setMaxSessionIdleTimeout(2048L * 2048L); container.setAsyncSendTimeout(2048L * 2048L); container.setMaxBinaryMessageBufferSize(2048 * 2048); return container; } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { final var heartBeatScheduler = new ThreadPoolTaskScheduler(); heartBeatScheduler.initialize(); /* define topic addresses for posting messages to clients */ registry .enableSimpleBroker(WS_SUBSCRIPTION_BASE_PATH) .setTaskScheduler(heartBeatScheduler) .setHeartbeatValue(new long[] { 0, 10000 }); /* prefix for websocket endpoints called by the clients */ registry.setApplicationDestinationPrefixes(WS_MESSAGE_ENDPOINT_BASE_PATH); } /** * Configures the API and topic prefixes for the websocket communication. * * @param registration the websocket transport registration */ @Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setMessageSizeLimit(300 * 1024 * 1024); // default : 64 * 1024 registration.setSendBufferSizeLimit(300 * 1024 * 1024); // default : 512 * 1024 registration.setSendTimeLimit(50 * 10000); // default : 10 * 10000 } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry .addEndpoint(RestApiEndpointConfig.WS_HANDSHAKE_BASE_PATH) // handshake URL .setAllowedOrigins("*"); } /** * Configure custom channel interceptors for the inbound websocket channel. * * @param registration the WS channel that shall be affected by the interceptor */ @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors( new WebsocketAuthenticationExtractingChannelInterceptor(this.jwtDecoder, this.authenticationConverter), new WebsocketLoggingChannelInterceptor(), new WebsocketStompHeaderExtractingChannelInterceptor() ); } }
As a client, I use rx-stomp, which is configured as follows:
stompClient.configure({ brokerURL: globalWebsocketState.brokerUrl, reconnectDelay: RECONNECT_DELAY_MILLIS, heartbeatIncoming: 0, heartbeatOutgoing: 10000, debug: (message: string) => { if (globalWebsocketState.showDebuggingMessages) { console.debug(message); } }, beforeConnect: async (client) => { const connectHeaders = new StompHeaders(); connectHeaders["Authorization"] = await _fetchAuthHeaderValue(); client.configure({ connectHeaders: connectHeaders, }); }, });
This results in the following behaviour:
The connection is established.
Then, the client can send 3 heartbeats within the first 30 seconds.
The last heartbeat results in an error message from the Spring Boot backend, resulting in a termination of the WS connection
The above can be seen in the following screenshot from the FireFox developer console as well:
Here are the logs of the Spring Boot backend:
2025-06-16T15:29:34.324+02:00 INFO 38992 --- [nio-9090-exec-3] d.g.c.b.R.CustomizedRequestLoggingFilter : STARTING REQUEST: [GET] - /ws 2025-06-16T15:29:34.325+02:00 TRACE 38992 --- [nio-9090-exec-3] o.s.w.s.s.s.WebSocketHandlerMapping : Mapped to HandlerExecutionChain with [org.springframework.web.socket.server.support.WebSocketHttpRequestHandler@2509259e] and 1 interceptors 2025-06-16T15:29:34.326+02:00 DEBUG 38992 --- [nio-9090-exec-3] o.s.w.s.s.s.WebSocketHttpRequestHandler : GET /ws 2025-06-16T15:29:34.326+02:00 TRACE 38992 --- [nio-9090-exec-3] o.s.w.s.s.s.DefaultHandshakeHandler : Processing request http://localhost:9090/ws with headers=[host:"localhost:9090", user-agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0", accept:"*/*", accept-language:"de,en-US;q=0.7,en;q=0.3", accept-encoding:"gzip, deflate, br, zstd", sec-websocket-version:"13", origin:"http://localhost:8500", sec-websocket-protocol:"v12.stomp, v11.stomp, v10.stomp", sec-websocket-extensions:"permessage-deflate", sec-websocket-key:"eeXP+3Tqesv73PHBpAMPdw==", connection:"keep-alive, Upgrade", sec-fetch-dest:"empty", sec-fetch-mode:"websocket", sec-fetch-site:"same-site", pragma:"no-cache", cache-control:"no-cache", upgrade:"websocket"] 2025-06-16T15:29:34.326+02:00 TRACE 38992 --- [nio-9090-exec-3] o.s.w.s.s.s.DefaultHandshakeHandler : Upgrading to WebSocket, subProtocol=v12.stomp, extensions=[] 2025-06-16T15:29:34.335+02:00 INFO 38992 --- [nio-9090-exec-3] d.g.c.b.R.CustomizedRequestLoggingFilter : FINISHED REQUEST: [GET] - /ws i> total execution time: 11ms 2025-06-16T15:29:34.342+02:00 DEBUG 38992 --- [nio-9090-exec-3] s.w.s.h.LoggingWebSocketHandlerDecorator : New StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] 2025-06-16T15:29:34.343+02:00 TRACE 38992 --- [nio-9090-exec-3] s.w.s.h.LoggingWebSocketHandlerDecorator : Handling TextMessage payload=[CONNECT Au..], byteCount=2725, last=true] in StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] 2025-06-16T15:29:34.344+02:00 TRACE 38992 --- [nio-9090-exec-3] o.s.w.s.m.StompSubProtocolHandler : From client: CONNECT session=7ff0a794-acd6-312f-3032-22f6206b3e60 2025-06-16T15:29:34.346+02:00 DEBUG 38992 --- [nio-9090-exec-3] s.s.i.WebsocketLoggingChannelInterceptor : WS >> CONNECT from "781bb818-9081-4dcb-8bc5-368d7808ea20" 2025-06-16T15:29:34.349+02:00 TRACE 38992 --- [ntextThread-193] o.s.w.s.adapter.NativeWebSocketSession : Sending TextMessage payload=[CONNECTED ..], byteCount=90, last=true], StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] 2025-06-16T15:29:44.363+02:00 TRACE 38992 --- [nio-9090-exec-5] s.w.s.h.LoggingWebSocketHandlerDecorator : Handling TextMessage payload=[ ], byteCount=1, last=true] in StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] 2025-06-16T15:29:44.363+02:00 TRACE 38992 --- [nio-9090-exec-5] o.s.w.s.m.StompSubProtocolHandler : From client: heart-beat in session 7ff0a794-acd6-312f-3032-22f6206b3e60 2025-06-16T15:29:54.370+02:00 TRACE 38992 --- [nio-9090-exec-7] s.w.s.h.LoggingWebSocketHandlerDecorator : Handling TextMessage payload=[ ], byteCount=1, last=true] in StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] 2025-06-16T15:29:54.371+02:00 TRACE 38992 --- [nio-9090-exec-7] o.s.w.s.m.StompSubProtocolHandler : From client: heart-beat in session 7ff0a794-acd6-312f-3032-22f6206b3e60 2025-06-16T15:30:04.434+02:00 TRACE 38992 --- [nio-9090-exec-8] s.w.s.h.LoggingWebSocketHandlerDecorator : Handling TextMessage payload=[ ], byteCount=1, last=true] in StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] 2025-06-16T15:30:04.434+02:00 TRACE 38992 --- [nio-9090-exec-8] o.s.w.s.m.StompSubProtocolHandler : From client: heart-beat in session 7ff0a794-acd6-312f-3032-22f6206b3e60 2025-06-16T15:30:09.180+02:00 TRACE 38992 --- [ntextThread-194] o.s.w.s.adapter.NativeWebSocketSession : Sending TextMessage payload=[ERROR mess..], byteCount=49, last=true], StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] 2025-06-16T15:30:09.181+02:00 DEBUG 38992 --- [ntextThread-194] o.s.w.s.adapter.NativeWebSocketSession : Closing StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] 2025-06-16T15:30:09.181+02:00 DEBUG 38992 --- [ntextThread-194] s.w.s.h.LoggingWebSocketHandlerDecorator : StandardWebSocketSession[id=7ff0a794-acd6-312f-3032-22f6206b3e60, uri=ws://localhost:9090/ws] closed with CloseStatus[code=1002, reason=null] 2025-06-16T15:30:09.181+02:00 DEBUG 38992 --- [ntextThread-194] o.s.w.s.m.SubProtocolWebSocketHandler : Clearing session 7ff0a794-acd6-312f-3032-22f6206b3e60 2025-06-16T15:30:09.182+02:00 INFO 38992 --- [ntextThread-194] u.s.CoreUserInteractionManagementService : Session for client 781bb818-9081-4dcb-8bc5-368d7808ea20 and session ID 7ff0a794-acd6-312f-3032-22f6206b3e60 ended. 2025-06-16T15:30:09.182+02:00 INFO 38992 --- [ntextThread-194] u.s.CoreUserInteractionManagementService : Removing all stale user interactions with session ID 7ff0a794-acd6-312f-3032-22f6206b3e60 from user interactions repository. 2025-06-16T15:30:09.182+02:00 INFO 38992 --- [ntextThread-194] u.s.CoreUserInteractionManagementService : No stale user interactions for session ID 7ff0a794-acd6-312f-3032-22f6206b3e60 were found. Aborting clean up process. 2025-06-16T15:30:09.182+02:00 DEBUG 38992 --- [ntextThread-194] s.s.i.WebsocketLoggingChannelInterceptor : WS >> DISCONNECT from "781bb818-9081-4dcb-8bc5-368d7808ea20" 2025-06-16T15:30:09.183+02:00 DEBUG 38992 --- [ntextThread-198] o.s.w.s.m.SubProtocolWebSocketHandler : No session for GenericMessage [payload=byte[0], headers={simpMessageType=DISCONNECT_ACK, simpDisconnectMessage=GenericMessage [payload=byte[0], headers={simpMessageType=DISCONNECT, stompCommand=DISCONNECT, simpSessionAttributes={org.springframework.messaging.simp.SimpAttributes.COMPLETED=true}, simpUser=CustomJwtAuth[Principal=xxx.core.security.model.CustomUserDetails@6ef1deb6, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[...]], simpSessionId=7ff0a794-acd6-312f-3032-22f6206b3e60}], simpUser=CustomJwtAuth [Principal=xxx.core.security.model.CustomUserDetails@6ef1deb6, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[...]], simpSessionId=7ff0a794-acd6-312f-3032-22f6206b3e60}]
Now the strange thing is:
When I only enable the Spring Boot backend to send PONG messages and disable client heartbeats, this does not happen. The WS connection stays stable.
Why is that?
EDIT: setting the client heartbeat interval to for example 60s results in the same result, it just takes longer: After 3 heartbeats, the server terminates the ws connection with 1002 error code.
See below picture
EDIT 2: Spring Version: 3.4.4