May be realted to https://github.com/spring-projects/spring-framework/issues/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