spring-projects/spring-ai · warning
Failed to complete SSE builder: + e.getMessage()
Error message
Failed to complete SSE builder: + e.getMessage()
What it means
A warning logged in WebMvcSseServerTransportProvider.closeGracefully when calling SseEmitter.complete() on the client's emitter throws (typically because the underlying connection is already closed, timed out, or the client disconnected). The lock is still released and the session continues closing; this warning only signals the SSE channel could not be completed cleanly.
Source
Thrown at mcp/transport/mcp-spring-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/transport/WebMvcSseServerTransportProvider.java:529
@Override
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return jsonMapper.convertValue(data, typeRef);
}
/**
* Initiates a graceful shutdown of the transport.
* @return A Mono that completes when the shutdown is complete
*/
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
this.sseBuilderLock.lock();
try {
this.sseBuilder.complete();
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to complete SSE builder: " + e.getMessage());
}
}
finally {
this.sseBuilderLock.unlock();
}
});
}
/**
* Closes the transport immediately.
*/
@Override
public void close() {
this.sseBuilderLock.lock();
try {
this.sseBuilder.complete();
}
catch (Exception e) {View on GitHub (pinned to 98a7beda4f)
Solutions
- Treat the warning as benign if clients are already disconnected — verify sessions were cleaned up
- Ensure closeGracefully is called once per session and coordinate with client disconnect callbacks
- Adjust servlet async/SSE timeout settings to exceed expected session lifetimes
- Catch and log client-disconnect conditions separately during shutdown to reduce noise
Example fix
// before
// shutdown hooks calling provider.closeGracefully() while clients still connected -> warnings
// after
// notify sessions / wait for client disconnects, then:
session.closeGracefully()
.doOnError(e -> log.debug("SSE already closed for session", e))
.block(Duration.ofSeconds(5)); Defensive patterns
Strategy: try-catch
Try / catch
session.closeGracefully()
.doOnError(e -> log.debug("SSE close skipped: {}", e.getMessage()))
.onErrorComplete()
.block(Duration.ofSeconds(5)); Prevention
- Expect and ignore close errors on already-disconnected clients
- Tune servlet async/SSE timeouts above session lifetime
- Call closeGracefully only once per session
- Coordinate shutdown with client disconnect callbacks
When it happens
Trigger: Client disconnected before server close; SSE emitter already completed or timed out (IllegalStateException); servlet container already invalidated the response; race between async timeout and graceful shutdown.
Common situations: Shutting down the MCP server while clients are connected; clients with flaky networks dropping mid-session; long-running sessions hitting servlet async timeouts; duplicate close calls on the same session.
Related errors
- MessageEndpoint must be set
- SSE connection '<connectionName>' requires a 'url' property.
- Failed to create SSE transport for connection '<connectionNa
- Error parsing JSON-RPC message:
- Failed to complete SSE builder for session + this.sessionId
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/ccffc250f13f014e.
Report an issue: GitHub.