spring-projects/spring-ai · warning
Failed to complete SSE builder for session + this.sessionId
Error message
Failed to complete SSE builder for session + this.sessionId + : + e.getMessage()
What it means
WebMvcStreamableServerTransportProvider.close() completes the SSE emitter for a streamable HTTP session under a lock. If completion throws (client disconnected, response committed/timed out), it logs this warning with the session id and swallows the exception. It means the stream for that session was not closed gracefully.
Source
Thrown at mcp/transport/mcp-spring-webmvc/src/main/java/org/springframework/ai/mcp/server/webmvc/transport/WebMvcStreamableServerTransportProvider.java:837
this.lock.lock();
try {
if (this.closed) {
if (logger.isDebugEnabled()) {
logger.debug("Session transport " + this.sessionId + " already closed");
}
return;
}
this.closed = true;
this.sseBuilder.complete();
if (logger.isDebugEnabled()) {
logger.debug("Successfully completed SSE builder for session " + this.sessionId);
}
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to complete SSE builder for session " + this.sessionId + ": " + e.getMessage());
}
}
finally {
this.lock.unlock();
}
}
}
/**
* Builder for creating instances of {@link WebMvcStreamableServerTransportProvider}.
*/
public static class Builder {
private @Nullable McpJsonMapper jsonMapper;
private String mcpEndpoint = "/mcp";
View on GitHub (pinned to 98a7beda4f)
Solutions
- Check the logged sessionId: if the client disconnected first, this warning is benign and can be ignored or logged at debug level
- Remove duplicate close() invocations for the same session (e.g. HttpDELETE handler plus shutdown cleanup) so the emitter is completed once
- Tune proxy/load-balancer idle timeouts to exceed your MCP session lifetime
- Raise spring.mvc.async.request-timeout so the container doesn't complete the response before the provider
Example fix
// before: both delete-session endpoint and shutdown hook close the session sessionFactory.onDelete(req -> provider.close(sessionId)); // shutdown: provider.close(sessionId); // second close -> warning // after: close idempotently via a session registry sessions.remove(sessionId).ifPresent(id -> provider.close(id));
Defensive patterns
Strategy: try-catch
Validate before calling
if (sessions.isActive(sessionId)) { sessions.close(sessionId); } Type guard
boolean sessionExists(String sessionId) { return sessionRegistry.containsKey(sessionId); } Try / catch
try {
provider.close(sessionId);
} catch (Exception e) {
logger.debug("Stream for session {} already completed", sessionId, e);
} Prevention
- Centralize session teardown in one code path to avoid double close
- Keep proxy/LB idle timeouts longer than the longest expected MCP session
- Raise spring.mvc.async.request-timeout appropriately
- Log sessionId on cleanup so warnings can be traced to client disconnects
When it happens
Trigger: Closing a streamable-HTTP MCP session whose SseEmitter was already completed by the client disconnecting, an async timeout, or a previous close() call; the session id is included in the message.
Common situations: MCP clients dropping connections mid-conversation; HTTP keep-alive/proxy timeouts killing the stream; duplicate session teardown (e.g. both a DELETE handler and a shutdown hook closing the same session); container restarts during load testing.
Related errors
- SSE connection '<connectionName>' requires a 'url' property.
- Failed to create SSE transport for connection '<connectionNa
- Stateless Streamable-Http prompt method must not declare par
- Stateless Streamable-Http prompt method must not declare par
- Stateless Streamable-Http prompt method must not declare par
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/cb8b89e195f1d642.
Report an issue: GitHub.