Skip to content

Handle closed session when sending MVC WebSocket response - #1517

Open
Meemaw wants to merge 1 commit into
spring-projects:mainfrom
Meemaw:handle-closed-session-in-send
Open

Handle closed session when sending MVC WebSocket response#1517
Meemaw wants to merge 1 commit into
spring-projects:mainfrom
Meemaw:handle-closed-session-in-send

Conversation

@Meemaw

@Meemaw Meemaw commented Aug 31, 2026

Copy link
Copy Markdown

Problem

In the WebMVC GraphQlWebSocketHandler, SendMessageSubscriber.hookOnNext catches only IOException from WebSocketSession.sendMessage:

try {
    this.session.sendMessage(nextMessage);
    request(1);
}
catch (IOException ex) {
    cancel();
    ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.session, ex, logger);
}

When the WebSocket session is closed while a subscription response is still in flight, the servlet container does not raise IOException — Tomcat raises IllegalStateException:

java.lang.IllegalStateException: Message will not be sent because the WebSocket session has been closed
	at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.writeMessagePart(WsRemoteEndpointImplBase.java:541)
	...
	at org.springframework.web.socket.adapter.standard.StandardWebSocketSession.sendTextMessage(StandardWebSocketSession.java:206)
	at org.springframework.web.socket.adapter.AbstractWebSocketSession.sendMessage(AbstractWebSocketSession.java:106)
	at org.springframework.graphql.server.webmvc.GraphQlWebSocketHandler$SendMessageSubscriber.hookOnNext(GraphQlWebSocketHandler.java:661)
	at reactor.core.publisher.BaseSubscriber.onNext(BaseSubscriber.java:163)
	at reactor.core.publisher.FluxPublishOn$PublishOnSubscriber.runAsync(FluxPublishOn.java:448)
	at reactor.core.publisher.FluxPublishOn$PublishOnSubscriber.run(FluxPublishOn.java:536)
	at reactor.core.scheduler.WorkerTask.call(WorkerTask.java:90)
	...

That IllegalStateException is not caught by the catch (IOException ...) clause, so instead of cancelling the response publisher and closing the session, it escapes hookOnNext and propagates on the serial send-scheduler thread (publishOn) as an uncaught error.

Production impact

We are hitting this in production. On our WebSocket subscription gateway (WebMVC + Tomcat), session close during an in-flight subscription is entirely routine — clients navigate away, connections drop — and every such race produces this stack trace. It is one of our highest-volume framework errors on that service (on the order of ~100k occurrences/week, in steady state), pure noise that drowns out real errors and, depending on the Reactor version, is logged as an uncaught error on the scheduler thread rather than being handled cleanly.

Fix

Handle IllegalStateException the same way as IOException in hookOnNext — cancel the response publisher and close the session with the error:

catch (IOException | IllegalStateException ex) {
    // IllegalStateException is raised (e.g. by Tomcat) when the session was
    // concurrently closed while a response was still in flight.
    cancel();
    ExceptionWebSocketHandlerDecorator.tryCloseWithError(this.session, ex, logger);
}

Test

Adds sessionClosedShouldCancelPublisher and a ClosedSession test fixture that lets the initial connection_ack through and then raises IllegalStateException on subsequent sends (simulating a session closed while a subscription response is in flight).

Note on observability: the reactor-core version used by the test suite wraps hookOnNext in BaseSubscriber.onNext and routes throwables to onOperatorError, which masks the escape at the handler-behaviour level. The regression test therefore asserts that the IllegalStateException is handled inside the subscriber and never reaches Reactor's Hooks.onOperatorError — this fails without the fix and passes with it.

Prior to this commit, SendMessageSubscriber.hookOnNext caught only
IOException from WebSocketSession.sendMessage. When the session is closed
while a subscription response is still in flight, the servlet container
(e.g. Tomcat) instead raises IllegalStateException ("Message will not be
sent because the WebSocket session has been closed"). That exception
escaped hookOnNext into reactor's error handling and, depending on the
reactor version, surfaced as an uncaught error on the serial send
scheduler thread instead of cancelling the response and closing the
session.

This commit also catches IllegalStateException in hookOnNext, handling
it the same as IOException: cancel the response publisher and close the
session with the error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Meemaw <ematej.snuderl@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 31, 2026
@bclozel bclozel added type: bug A general bug in: web Issues related to web handling and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 31, 2026
@bclozel bclozel self-assigned this Aug 31, 2026
@bclozel bclozel added this to the 2.0.6 milestone Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: web Issues related to web handling type: bug A general bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants