fix(server): release the standalone GET stream on session teardown - #967
fix(server): release the standalone GET stream on session teardown#967lArtiquel wants to merge 1 commit into
Conversation
…otocol#922) The standalone GET handler parks on awaitCancellation() for the lifetime of the stream. Tearing the session down only called ServerSSESession.close(), which flushes and closes the response body but leaves that coroutine suspended, so the ApplicationCall and its connection were never released — one leaked socket per completed session. Cancel the call alongside closing the session, both when the transport is closed and when a reconnecting client replaces the standalone stream.
There was a problem hiding this comment.
Pull request overview
This PR fixes a resource leak in the stateful StreamableHttpServerTransport where the standalone GET SSE handler coroutine remained suspended on awaitCancellation() even after session teardown or stream replacement, leaving the underlying ApplicationCall/socket unreleased. It adds lifecycle tests to ensure the GET handler is still suspended before teardown and is actually released when the transport is closed or the stream is replaced.
Changes:
- Track each GET stream’s
ApplicationCalland explicitly cancel its backing Job on transportclose()to release the connection. - On replacement of the standalone GET stream, close the old SSE session and also cancel the old call Job.
- Add JVM tests covering transport teardown and GET-stream replacement lifecycle behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StreamableHttpServerTransport.kt | Cancels the standalone GET stream’s call Job on session teardown and on stream replacement to prevent leaked server-side connections. |
| kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatefulStreamableHttpGetStreamLifecycleTest.kt | Adds regression tests asserting the GET handler remains suspended until teardown/replacement and is released afterward. |
Suppressed comments (1)
kotlin-sdk-server/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/StatefulStreamableHttpGetStreamLifecycleTest.kt:124
- This test launches a standalone
CoroutineScope(Dispatchers.Default)for the GET stream but never cancels it. If the test fails beforetransport.close()(or if the client call hangs), this coroutine can leak past the test lifecycle and cause flakiness in subsequent tests. Tie the launched coroutine to the current test coroutine context instead of creating an independent scope.
val scope = CoroutineScope(Dispatchers.Default)
scope.launch { openGetStream(assertNotNull(sessionId)) }
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| val scope = CoroutineScope(Dispatchers.Default) | ||
| scope.launch { openGetStream(sessionId) } | ||
|
|
||
| awaitRelease(firstReleased) shouldBe true | ||
| scope.cancel() | ||
| } |
Fixes #922.
handleGetRequestparks onawaitCancellation()for the lifetime of the standalone GET stream.close()only calledServerSSESession.close(), which flushes and closes the response body but never cancels that coroutine, so theApplicationCalland its connection were never released — one leaked socket per completed session, as measured in the issue.SessionContextalready holds theApplicationCall, so this cancels its job alongside closing the session.Same assumption appeared a second time in the GET-replacement path, where the comment claimed
session.close()cancels the old coroutine. It doesn't, so a client reconnecting its GET stream leaked the previous one the same way. Fixed and the comment corrected.Two tests, both failing before the change: one for teardown via
close(), one for a replacement stream. Both assert the handler is still suspended before teardown, otherwise they'd pass vacuously — the client-side view can't catch this, sinceflushAndClose()gives the client EOF while the server coroutine stays parked.ktlintCheck,detekt,apiCheckand the JVM test suites pass.