Skip to content

UnboundLocalError in send_request when response_or_error is not assigned #1717

@maxisbey

Description

@maxisbey

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() raises TimeoutError → caught, McpError raised, 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:

  1. receive() is executing
  2. Timeout deadline is exceeded at the exact moment
  3. Cancellation exception is raised but incorrectly suppressed by the cancel scope
  4. Execution continues as if the try block succeeded
  5. 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 side
  • ClosedResourceError - 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+

AI Disclaimer

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Significant bug affecting many users, highly requested featurebugSomething isn't workingready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions