RustPython/RustPython · error · RuntimeError

cannot switch state from {} to {}

Error message

cannot switch state from {} to {}

What it means

SSLProtocol is a small state machine (UNINIT -> HANDSHAKING -> post-handshake -> FLUSHING -> SHUTDOWN on the normal path). _switch_state permits only legal transitions (the visible one being FLUSHING->SHUTDOWN) and raises RuntimeError('cannot switch state from X to Y') for everything else. Seeing it means connection/data events arrived in an order the machine forbids: broken custom transport glue, misuse of private SSLProtocol methods, or a core bug.

Source

Thrown at Lib/asyncio/sslproto.py:530

            allowed = True

        elif (
            self._state == SSLProtocolState.WRAPPED and
            new_state == SSLProtocolState.FLUSHING
        ):
            allowed = True

        elif (
            self._state == SSLProtocolState.FLUSHING and
            new_state == SSLProtocolState.SHUTDOWN
        ):
            allowed = True

        if allowed:
            self._state = new_state

        else:
            raise RuntimeError(
                'cannot switch state from {} to {}'.format(
                    self._state, new_state))

    # Handshake flow

    def _start_handshake(self):
        if self._loop.get_debug():
            logger.debug("%r starts SSL handshake", self)
            self._handshake_start_time = self._loop.time()
        else:
            self._handshake_start_time = None

        self._set_state(SSLProtocolState.DO_HANDSHAKE)

        # start handshake timeout count down
        self._handshake_timeout_handle = \
            self._loop.call_later(self._ssl_handshake_timeout,
                                  self._check_handshake_timeout)

View on GitHub (pinned to aaeab4f754)

Solutions

  1. Upgrade Python/RustPython; a number of 'cannot switch state' reports trace to shutdown races fixed in later releases.
  2. Remove direct calls to SSLProtocol private methods (_start_shutdown, _switch_state, _force_close); drive teardown with transport.close() or abort().
  3. Ensure protocol callbacks fire on the event-loop thread and at most once per connection (use call_soon_threadsafe from other threads).
  4. If reproducible with unmodified asyncio, capture a minimal reproducer and report upstream.

Example fix

# before
proto._start_shutdown()  # illegal from some states -> RuntimeError: cannot switch state

# after
proto._transport.close()  # public path; the state machine walks itself to SHUTDOWN
Defensive patterns

Strategy: try-catch

Validate before calling

# before driving shutdown through private APIs, prefer the public path
assert not hasattr(proto, '_start_shutdown') or True  # never call private hooks directly
# correct shutdown:
proto._transport.close()  # state machine walks itself to SHUTDOWN

Try / catch

try:
    transport.close()
    await asyncio.wait_for(proto._get_app_transport().wait_closed(), timeout=5)
except RuntimeError as e:
    if 'cannot switch state' in str(e):
        logger.error('SSL state machine desync: %s', e)
        transport.abort()  # force-close; connection is unrecoverable
    else:
        raise

Prevention

When it happens

Trigger: Calling private hooks out of order: _start_shutdown() from a state that must go through FLUSHING, eof/feed_eof delivered before the handshake completes, data_received after connection_lost; custom transports emitting events twice or off the loop thread; protocol instance reuse; occasionally genuine shutdown races in older asyncio point releases.

Common situations: Monkeypatched event loops in test suites; custom transports in embedded interpreters (e.g. RustPython hosts) that mis-sequence callbacks; code reaching into _start_shutdown/_abort directly; older Python versions missing SSL shutdown race fixes.

Related errors


AI-assisted analysis of RustPython/RustPython@aaeab4f754 (2026-08-17). Data as JSON: /api/errors/e5ecf505c5ae0066. Report an issue: GitHub.