fix: release transport-owned HttpClient references on close by TsengX · Pull Request #904 · modelcontextprotocol/java-sdk
Summary
Fixes the Java 17 HttpClient-* SelectorManager thread retention reported in #547 and #620 by releasing transport-owned HttpClient references on close instead of forcing internal client shutdown.
This change:
- preserves the current public builder API
- keeps Java 17 compatibility
- avoids reflection,
Unsafe, and JDK-internal shutdown hooks - keeps
closeGracefully()non-blocking - adds deterministic Docker-free leak reproducer tests for both SSE and streamable HTTP transports
This is an alternative to the approaches explored in #610 and #868.
Motivation and Context
The root problem was that HttpClientSseClientTransport and HttpClientStreamableHttpTransport retained a strong reference to their internally created HttpClient even after transport close.
If user code retained closed transport objects, those internal HttpClient instances also remained reachable, which in turn kept HttpClient-* SelectorManager threads alive.
Instead of trying to forcibly shut down JDK HttpClient internals, this PR treats the issue as an ownership problem:
- transports own the internally created
HttpClient - once the transport is closed, that owned reference is released
- after that, the client becomes eligible for normal JVM cleanup
Additionally, DefaultMcpTransportSession.closeGracefully() now always disposes tracked connections even if onClose fails, which makes close-path cleanup reliable.
How Has This Been Tested?
Locally verified with targeted mcp-core regression tests:
DefaultMcpTransportSessionTestsHttpClientSseClientTransportLeakTestsHttpClientStreamableHttpTransportLeakTests
These tests:
- repeatedly create and close transports
- intentionally keep closed transport objects reachable
- assert that
HttpClient-* SelectorManagerthreads return to baseline after GC stabilization
Also verified with existing Docker-backed integration tests:
HttpClientSseClientTransportTestsHttpClientStreamableHttpTransportTest
Breaking Changes
None.
Types of changes
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to change)
- Documentation update
Checklist
- I have read the [MCP Documentation (https://modelcontextprotocol.io/)
- My code follows the repository's style guidelines
- New and existing tests pass locally
- I have added appropriate error handling
- I have added or updated documentation as needed
Additional context
Implementation notes:
- Added an internal
OwnedHttpClientabstraction to track transport-owned clients. HttpClientSseClientTransportandHttpClientStreamableHttpTransportnow release their owned client reference on close.DefaultMcpTransportSession.closeGracefully()now disposes tracked connections even whenonCloseerrors.- The fix intentionally avoids reflection-based shutdown of JDK
HttpClientinternals.