Skip to content

Fix: swallow _receiveTask exceptions in SseClientSessionTransport.Clo…#1432

Open
xue-cai wants to merge 1 commit intomodelcontextprotocol:mainfrom
xue-cai:fix/sse-connect-close-exception-swallow
Open

Fix: swallow _receiveTask exceptions in SseClientSessionTransport.Clo…#1432
xue-cai wants to merge 1 commit intomodelcontextprotocol:mainfrom
xue-cai:fix/sse-connect-close-exception-swallow

Conversation

@xue-cai
Copy link

@xue-cai xue-cai commented Mar 14, 2026

…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 _receiveTask in 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

  • 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
  • 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

…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 _receiveTask` in 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.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

1 participant