Fix: swallow _receiveTask exceptions in SseClientSessionTransport.Clo…#1432
Open
xue-cai wants to merge 1 commit intomodelcontextprotocol:mainfrom
Open
Fix: swallow _receiveTask exceptions in SseClientSessionTransport.Clo…#1432xue-cai wants to merge 1 commit intomodelcontextprotocol:mainfrom
xue-cai wants to merge 1 commit intomodelcontextprotocol:mainfrom
Conversation
…seAsync
When SseClientSessionTransport.ConnectAsync fails (e.g. server returns 405),
the catch block calls CloseAsync() before wrapping the error in
InvalidOperationException. However, CloseAsync() awaits the already-faulted
_receiveTask without a try-catch, causing the original HttpRequestException
to propagate out of CloseAsync and preempt the InvalidOperationException
wrapping. Callers then receive a bare HttpRequestException instead of the
documented InvalidOperationException("Failed to connect transport", ex).
The fix wraps `await _receiveTask` in CloseAsync with a try-catch to
swallow already-observed exceptions from the faulted task.
Code references:
SseClientSessionTransport.ConnectAsync (catch block calls CloseAsync):
https://github.com/modelcontextprotocol/csharp-sdk/blob/v0.9.0-preview.2/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs#L52-L67
SseClientSessionTransport.CloseAsync (awaits _receiveTask without try-catch):
https://github.com/modelcontextprotocol/csharp-sdk/blob/v0.9.0-preview.2/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs#L108-L130
Testing:
# Build all frameworks (only netstandard2.0 fails — pre-existing on main):
dotnet build tests/ModelContextProtocol.Tests/ '/p:NoWarn=NU1903%3BMCPEXP001'
# Run full transport suite (74 tests × 3 frameworks = 222 total, 0 failures):
dotnet test tests/ModelContextProtocol.Tests/ '/p:NoWarn=NU1903%3BMCPEXP001' --no-build --filter "FullyQualifiedName~Transport" --framework net10.0
dotnet test tests/ModelContextProtocol.Tests/ '/p:NoWarn=NU1903%3BMCPEXP001' --no-build --filter "FullyQualifiedName~Transport" --framework net9.0
dotnet test tests/ModelContextProtocol.Tests/ '/p:NoWarn=NU1903%3BMCPEXP001' --no-build --filter "FullyQualifiedName~Transport" --framework net8.0
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
9b21522 to
056f1f5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
…seAsync
When SseClientSessionTransport.ConnectAsync fails (e.g. the SSE GET returns 405), its catch block calls CloseAsync() before wrapping the error in InvalidOperationException("Failed to connect transport", ex).
However, CloseAsync() awaits _receiveTask which is already faulted with the same HttpRequestException. Since there is no try-catch around that await, the exception propagates out of CloseAsync, out of the catch block, and the InvalidOperationException wrapping is never reached.
This means callers of ConnectAsync receive an unwrapped HttpRequestException instead of the documented InvalidOperationException wrapper, which breaks exception filtering in downstream code.
The fix wraps
await _receiveTaskin CloseAsync with a try-catch that swallows both OperationCanceledException (normal shutdown) and other exceptions (already observed and forwarded via_connectionEstablished.TrySetException in ReceiveMessagesAsync).
Includes a regression test that verifies ConnectAsync throws InvalidOperationException (not HttpRequestException) when both Streamable HTTP POST and SSE GET fallback fail.
Motivation and Context
How Has This Been Tested?
Breaking Changes
Types of changes
Checklist
Additional context