{"record":{"id":"e5ecf505c5ae0066","repo":"RustPython/RustPython","slug":"cannot-switch-state-from-to","errorCode":null,"errorMessage":"cannot switch state from {} to {}","messagePattern":"cannot switch state from (.+?) to (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"Lib/asyncio/sslproto.py","lineNumber":530,"sourceCode":"            allowed = True\n\n        elif (\n            self._state == SSLProtocolState.WRAPPED and\n            new_state == SSLProtocolState.FLUSHING\n        ):\n            allowed = True\n\n        elif (\n            self._state == SSLProtocolState.FLUSHING and\n            new_state == SSLProtocolState.SHUTDOWN\n        ):\n            allowed = True\n\n        if allowed:\n            self._state = new_state\n\n        else:\n            raise RuntimeError(\n                'cannot switch state from {} to {}'.format(\n                    self._state, new_state))\n\n    # Handshake flow\n\n    def _start_handshake(self):\n        if self._loop.get_debug():\n            logger.debug(\"%r starts SSL handshake\", self)\n            self._handshake_start_time = self._loop.time()\n        else:\n            self._handshake_start_time = None\n\n        self._set_state(SSLProtocolState.DO_HANDSHAKE)\n\n        # start handshake timeout count down\n        self._handshake_timeout_handle = \\\n            self._loop.call_later(self._ssl_handshake_timeout,\n                                  self._check_handshake_timeout)","sourceCodeStart":512,"sourceCodeEnd":548,"githubUrl":"https://github.com/RustPython/RustPython/blob/aaeab4f754b4f40efc0c8ab39cf7c4a3c35a8cfd/Lib/asyncio/sslproto.py#L512-L548","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nproto._start_shutdown()  # illegal from some states -> RuntimeError: cannot switch state\n\n# after\nproto._transport.close()  # public path; the state machine walks itself to SHUTDOWN","handlingStrategy":"try-catch","validationCode":"# before driving shutdown through private APIs, prefer the public path\nassert not hasattr(proto, '_start_shutdown') or True  # never call private hooks directly\n# correct shutdown:\nproto._transport.close()  # state machine walks itself to SHUTDOWN","typeGuard":null,"tryCatchPattern":"try:\n    transport.close()\n    await asyncio.wait_for(proto._get_app_transport().wait_closed(), timeout=5)\nexcept RuntimeError as e:\n    if 'cannot switch state' in str(e):\n        logger.error('SSL state machine desync: %s', e)\n        transport.abort()  # force-close; connection is unrecoverable\n    else:\n        raise","preventionTips":["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."],"tags":["asyncio","ssl","state-machine","internal"],"backgroundTag":"invalid-state-transition","analyzedSha":"aaeab4f754b4f40efc0c8ab39cf7c4a3c35a8cfd","analyzedAt":"2026-08-17T00:37:52.100Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}