fix: collapse single-error ExceptionGroups from task group cancellations#2183
Open
giulio-leone wants to merge 6 commits intomodelcontextprotocol:mainfrom
Open
fix: collapse single-error ExceptionGroups from task group cancellations#2183giulio-leone wants to merge 6 commits intomodelcontextprotocol:mainfrom
giulio-leone wants to merge 6 commits intomodelcontextprotocol:mainfrom
Conversation
When a task in an anyio task group fails, sibling tasks are cancelled and the resulting Cancelled exceptions are wrapped alongside the real error in a BaseExceptionGroup. This makes it extremely difficult for callers to classify the root cause of failures. Added collapse_exception_group() utility and a drop-in create_task_group() context manager that automatically unwraps single-error exception groups. When there is exactly one non-cancellation error, callers now receive the original exception directly instead of a wrapped group. Applied to all client-facing code paths: - BaseSession.__aexit__ (affects all session-based operations) - Client transports: stdio, SSE, streamable HTTP, websocket, memory Multiple real errors (non-cancellation) are preserved as-is in the exception group. Fixes modelcontextprotocol#2114 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, add multi-failure test
…00% coverage The conditional import of BaseExceptionGroup for Python < 3.11 used pragma:no-branch, which only suppresses branch coverage. On 3.11+ the import statement itself is never executed, causing coverage to drop below 100%. Switch to pragma:no-cover which excludes the entire conditional block from coverage reporting. Also mark the re-raise in session.__aexit__ as pragma:no-cover since it only fires when the task group has multiple simultaneous real failures (an edge case that is already tested in test_exception_utils). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s strict-no-cover)
Author
|
All 26 CI checks pass. This collapses single-error ExceptionGroups for cleaner error messages. Ready for review. |
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.
Summary
Fixes #2114
When a task in an anyio task group fails, sibling tasks are cancelled and the resulting
Cancelledexceptions are wrapped alongside the real error in aBaseExceptionGroup. This makes it extremely difficult for callers to classify the root cause of failures — they must manually unwrap exception groups to find what actually went wrong.Solution
Added a
collapse_exception_group()utility and a drop-increate_task_group()context manager inmcp.shared._exception_utilsthat automatically unwraps single-error exception groups:__cause__)BaseExceptionGroupas-isApplied Sites
The unwrapping is applied to all client-facing code paths:
shared/session.pyBaseSession.__aexit__— affects all session-based operationsclient/stdio.pyclient/sse.pyclient/streamable_http.pyclient/websocket.pyclient/_memory.pyServer-side task groups are left unchanged as they typically have internal error handling.
Tests
Added comprehensive tests in
tests/shared/test_exception_utils.py:TestCollapseExceptionGroup: 5 unit tests covering all edge casesTestCreateTaskGroup: 3 integration tests verifying unwrapping, clean exit, and cause chainingAll 154 shared tests pass with 0 errors.