Conversation
…ound transport send blocks indefinitely when response arrives via background GET SSE stream Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
|
@copilot please update the PR description with a detailed outline of the problem, and fix the PR title. |
Updated the PR description with a detailed outline covering how |
|
@copilot there are a bunch of CI failures |
The CI failures are pre-existing and not introduced by this change. The latest main branch CI run on the same base commit ( All jobs in both runs show results like I verified locally: 0 failures with 1823 + 285 passed tests. |
…ted, preventing masking of transport errors like connection closure Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
|
@copilot there are still hangs and failures across all of the CI legs |
… and transport cancellation issues Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
The ContinueWith+CTS approach had two bugs: (1) a disposal race where the ContinueWith callback calls Replaced with a simpler |
|
@copilot every CI leg failed |
|
@copilot every CI leg failed |
Problem
McpSessionHandler.SendRequestAsynccan hang indefinitely in the Streamable HTTP transport due to a race between the foreground transport send and the background GET SSE stream.How
SendRequestAsyncworksIn
SendRequestAsync, aTaskCompletionSource(TCS) is registered in_pendingRequests, thenSendToRelatedTransportAsyncisawaited. For the Streamable HTTP transport,SendToRelatedTransportAsynccallsStreamableHttpClientSessionTransport.SendMessageAsync, which POSTs the JSON-RPC request and reads the response. If the POST response is an SSE stream that ends without a reply (e.g. the server sends only a priming event), the transport enters a retry loop issuing GET requests withLast-Event-IDuntil it receives the response.Separately, a background GET SSE task (
ReceiveUnsolicitedMessagesAsync) is started as fire-and-forget after initialization to receive server-initiated messages. This task continuously issues GET requests and processes any incoming JSON-RPC messages, routing responses through_pendingRequests.The race
Under load (e.g. CI with thread pool starvation), the background GET's async continuations can be delayed while the foreground
ListToolsAsync+CallToolAsyncpath runs promptly (it's being actively awaited). This creates a window where:tools/callPOST arrives at the server and the server records a pending tool responseSendToRelatedTransportAsyncnever returns, so the already-completed TCS is never awaitedFix
Replace the original
await SendToRelatedTransportAsync(request, cancellationToken)withTask.WhenAny(sendTask, tcs.Task). If the response TCS completes first (via a concurrent background channel), execution proceeds immediately without waiting for the foreground send. If the send completes first (normal case), it is awaited directly, preserving the original behavior. The still-running send task's exceptions are observed via a fire-and-forgetContinueWith(OnlyOnFaulted)to prevent unobserved task exceptions.💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.