Repository navigation
DefaultMcpTransportSession#closeGracefully leaks resources on incomplete DELETE response #547
Description
Activity
- addedwaiting for userWaiting for user feedback or more detailsWaiting for user feedback or more details
on Feb 20, 2026 Is this perhaps related to the inability to close the HttpClient properly in Java 17?
- marked HttpClient resource leak causes thread accumulation and memory exhaustion #620 as a duplicate of this issue
on Feb 20, 2026 An estensive analysis was published in the duplicate issue: #620 (comment)
- addedbugSomething isn't workingSomething isn't workingP2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable featureand removedbugSomething isn't workingSomething isn't workingwaiting for userWaiting for user feedback or more detailsWaiting for user feedback or more details
on Feb 20, 2026 #566 covers the case where the DELETE fails. It does not cover the case where the DELETE never answers, and that is the one that leaks.
closeGracefully()is:return Mono.from(this.onClose.apply(this.sessionId.get())) .then(Mono.fromRunnable(this.openConnections::dispose));
#566 wraps the first stage in
onErrorResumeand keeps the.then(...). A DELETE that hangs signals no error, sodispose()is never reached.There is no second chance either.
HttpClientStreamableHttpTransport.closeGracefully()replacesactiveSessionwith a closed session before it awaits the close, so once the close stops progressing, the session holding theDisposableis unreachable. Each occurrence strands one SSE subscription and its socket. On one of our production pods that came to roughly 1,400 stranded outbound sockets over a business day.Easy to reproduce: point the transport at a stub that returns the DELETE response headers and never completes the body.
closeGracefully()does not return andopenConnectionsis never disposed.Two ways to fix it, as far as I can see: a timeout on the
onClosepublisher plus an unconditional dispose, or dispose before attempting the DELETE. We run the first against 1.1.0, with dispose on success, error, timeout and cancellation.If you take the timeout route, cancelling that
Monodoes not leave the exchange running: reactor'sMonoCompletionStagecallsfuture.cancel(true)and the JDK forwards it to the exchange, which closes the connection. Two things that tripped us up while verifying that.isCancelled()stays false afterwards, because what gets stored is aCompletionExceptionwrapping the cancellation. AndfromFutureversusfromCompletionStagemakes no difference here, both passsuppressCancellation=falsein reactor-core 3.8.4.Happy to send the test, either as a PR here or into #566.
- unmarked HttpClient resource leak causes thread accumulation and memory exhaustion #620 as a duplicate of this issue
on Aug 27, 2026 - changed the title
[-]DefaultMcpTransportSession.java closeGracefully may leak resources[/-][+]DefaultMcpTransportSession#closeGracefully leaks resources on incomplete DELETE[/+]on Aug 27, 2026 - changed the title
[-]DefaultMcpTransportSession#closeGracefully leaks resources on incomplete DELETE[/-][+]DefaultMcpTransportSession#closeGracefully leaks resources on incomplete DELETE response[/+]on Aug 27, 2026 Hey @andersenleo thanks for the extra info (great writeup!)
Please shared the test as a snippet directly in this issue.


Bug description
When the MCP server is abnormal, the statement this.onClose.apply(this.sessionId.get()) in the closeGracefully method of DefaultMcpTransportSession.java throws an exception, so this.openConnections::dispose is never executed and may leak resources.
Environment
Java version: 21
mcp-bom: 0.11.3
Steps to reproduce
1、Create an McpSyncClient.
2、Execute client.initialize(); client.listTools();
3、Suspend the debugger at client.closeGracefully();
4、Make the MCP server unavailable.
5、Resume execution of client.closeGracefully();
Expected behavior
this.openConnections::dispose should always be executed.
Minimal Complete Reproducible example
McpSyncClient client = McpClient.sync(transport)
.requestTimeout(Duration.ofSeconds(10))
.build();
// Initialize connection
client.initialize();
// List available tools
ListToolsResult tools = client.listTools();
// Close client
client.closeGracefully();