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
- Upgrade Python/RustPython; a number of 'cannot switch state' reports trace to shutdown races fixed in later releases.
- Remove direct calls to SSLProtocol private methods (_start_shutdown, _switch_state, _force_close); drive teardown with transport.close() or abort().
- Ensure protocol callbacks fire on the event-loop thread and at most once per connection (use call_soon_threadsafe from other threads).
- 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
- Never call SSLProtocol private methods; drive shutdown via transport.close()/abort().
- From other threads, schedule protocol callbacks with loop.call_soon_threadsafe so ordering holds.
- Pin recent patch releases of Python/RustPython, which contain SSL shutdown race fixes.
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
- Creating _SSLProtocolTransport twice
- 2nd arg must be a tuple
- ObjC runtime library couldn't be loaded
- Socket cannot be of type SSLSocket
- server_hostname is only meaningful with ssl
AI-assisted analysis of RustPython/RustPython@aaeab4f754 (2026-08-17).
Data as JSON: /api/errors/e5ecf505c5ae0066.
Report an issue: GitHub.