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

  1. Check the logged sessionId: if the client disconnected first, this warning is benign and can be ignored or logged at debug level
  2. Remove duplicate close() invocations for the same session (e.g. HttpDELETE handler plus shutdown cleanup) so the emitter is completed once
  3. Tune proxy/load-balancer idle timeouts to exceed your MCP session lifetime
  4. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/cb8b89e195f1d642. Report an issue: GitHub.