spring-projects / spring-projects/spring-framework
Simple Websocket Broker terminates WS connection with 1002 error code when client sends heartbeats
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 60.2k
- Forks
- 38.8k
- Avg merge
- 5d 2h
- Merged PRs (30d)
- 27
Description
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the heartbeat configuration in configureMessageBroker and the StompSubProtocolHandler logs showing the third client heartbeat followed by ERROR and close code 1002. Reproduce the behavior with the provided rx-stomp settings, then compare it with server-only heartbeats and the 60-second interval. Done means explaining why client heartbeats trigger termination and identifying the Spring Framework behavior involved.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100