Skip to content

fix(core): remove pending response entries on request timeout and cancellation - #1134

Open
Lubaoshuai wants to merge 2 commits into
modelcontextprotocol:mainfrom
Lubaoshuai:fix/pending-response-timeout-leak
Open

Lubaoshuai wants to merge 2 commits into
modelcontextprotocol:mainfrom
Lubaoshuai:fix/pending-response-timeout-leak

Conversation

@Lubaoshuai

Copy link
Copy Markdown

Fixes #1133

McpClientSession.sendRequest and McpServerSession.sendRequest left their pendingResponses entry in place when the downstream .timeout(...) fired or the caller cancelled — the entry was only removed on a response or a send failure. Since request IDs are unique per request, a timed-out request leaked its entry forever; KeepAliveScheduler pings amplify this into one leaked entry per interval on sessions with a dead peer.

Both methods now mirror the cleanup pattern already used by McpStreamableServerSession.McpStreamableServerSessionStream.sendRequest: doOnError + doOnCancel remove the entry after the timeout boundary. Removal is idempotent, and a late response for a removed id already takes the existing "unexpected response" path.

How verified

  • New testRequestTimeoutRemovesPendingResponse in McpClientSessionTests: a request against a 50 ms-timeout session is let expire, then the pendingResponses map is asserted empty.
  • ./mvnw -pl mcp-core test passes locally.

…cellation

McpClientSession and McpServerSession left the pendingResponses entry in
place when the downstream timeout fired or the caller cancelled, so each
timed-out request leaked its entry forever (request IDs are unique).
Mirror the doOnError/doOnCancel cleanup the streamable session variant
already performs after .timeout(requestTimeout).

Fixes modelcontextprotocol#1133
@Lubaoshuai
Lubaoshuai force-pushed the fix/pending-response-timeout-leak branch from b25d2cc to 9b8604e Compare September 13, 2026 18:03

Copy link
Copy Markdown

The cleanup placement looks consistent with the streamable-session pattern, and the idempotent remove makes the duplicate send-error cleanup harmless.

One regression-coverage gap seems worth closing before merge: this PR changes both McpClientSession.sendRequest and McpServerSession.sendRequest, and the stated fix covers timeout + downstream cancellation, but the added test exercises only client timeout.

I would add at least:

  1. a server-side timeout regression asserting McpServerSession.pendingResponses is empty after the request timeout; and
  2. a cancellation regression where the subscriber cancels a request before a response arrives, then asserts the pending entry is gone.

If inexpensive, exercising cancellation on both client and server would pin the exact contract this PR is adding. The existing reflection-based assertion style is sufficient for this focused regression; no public test hook seems necessary.

That would protect against a future refactor preserving timeout cleanup while accidentally dropping the doOnCancel behavior, and would also prove the server-side change rather than relying on symmetry with the client.

…se cleanup

Per review: the fix touches both McpClientSession and McpServerSession
but only client-side timeout had regression coverage. Add:
- McpServerSessionTests: pendingResponses is empty after request
  timeout, and after the subscriber cancels before a response arrives
- McpClientSessionTests: pending entry removed on subscriber cancel

spring-javaformat applied; mcp-core targeted suite: 15/15 green.

Signed-off-by: Lubaoshuai <128781758+Lubaoshuai@users.noreply.github.com>
@Lubaoshuai

Copy link
Copy Markdown
Author

Thanks for the careful review — agreed on both points. Pushed in 6be0c53:

  1. Server-side timeout regression — new McpServerSessionTests (there was no dedicated spec-level server session test class yet): a server session with a 50 ms request timeout against a transport that accepts the message but never responds; after the TimeoutException surfaces, pendingResponses is asserted empty.
  2. Cancellation regression, both sides — McpServerSessionTests.testRequestCancellationRemovesPendingResponse and McpClientSessionTests.testRequestCancellationRemovesPendingResponse: subscribe while the response is still pending (entry present, asserted hasSize(1)), dispose, and assert the pending entry is removed by the doOnCancel handler on each session type.

All three use the same reflection-based pendingResponses assertion as the existing client timeout test. spring-javaformat:apply run on the new file, and the targeted mcp-core suite is green (15/13+2 tests).

Copy link
Copy Markdown

Verified the updated head. The new server-side timeout regression is present, and cancellation cleanup is now covered on both client and server with the pending map asserted non-empty before cancellation and empty after disposal.

That closes the coverage gap I raised. Thanks for adding the symmetric regressions.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

McpClientSession/McpServerSession: pending response entries leak when a request times out or is cancelled

2 participants