-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
Description
We received a report of intermittent UnboundLocalError exceptions occurring in send_request():
UnboundLocalError: cannot access local variable 'response_or_error' where it is not associated with a value
The error originates from mcp/shared/session.py at the line that checks isinstance(response_or_error, JSONRPCError).
Code Structure
The current code structure in send_request() is:
try:
with anyio.fail_after(timeout):
response_or_error = await response_stream_reader.receive()
except TimeoutError:
raise McpError(...)
# response_or_error is used here, outside the inner try/except
if isinstance(response_or_error, JSONRPCError):
raise McpError(response_or_error.error)Analysis
For UnboundLocalError to occur, execution must reach the isinstance check without response_or_error ever being assigned. Under normal Python semantics this should be impossible:
- If
receive()completes → variable is assigned - If
receive()raisesTimeoutError→ caught,McpErrorraised, never reaches the check - If
receive()raises other exceptions → propagates up, never reaches the check
Theories
1. anyio fail_after race condition
There was a documented race condition in anyio (agronholm/anyio#589) where fail_after() could incorrectly suppress exceptions. The scenario:
receive()is executing- Timeout deadline is exceeded at the exact moment
- Cancellation exception is raised but incorrectly suppressed by the cancel scope
- Execution continues as if the try block succeeded
- But the assignment never completed
This was reportedly fixed in anyio 4.0 via PR #591, but there may be remaining edge cases.
2. Unhandled exceptions from receive()
MemoryObjectReceiveStream.receive() can raise:
EndOfStream- if stream closed from sender sideClosedResourceError- if stream explicitly closed
Neither is caught by except TimeoutError, so they would propagate. However, this should prevent reaching the isinstance check, not cause UnboundLocalError.
3. Exception group interaction (Python 3.11+)
With task groups, exceptions can be wrapped in ExceptionGroup. The except TimeoutError clause does not catch ExceptionGroup containing TimeoutError. However, this still shouldn't allow execution to continue to the isinstance check.
Environment
- Reported on: anyio >= 4.5
- Python: 3.10+