{"record":{"id":"a316e263013a4164","repo":"python/cpython","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/python/cpython/blob/bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6/Lib/asyncio/sslproto.py#L512-L548","documentation":"Raised by _SSLProtocol._switch_state() as RuntimeError when the SSL state machine is asked to move to a state that the current state does not permit (the allowed transitions are the explicit UNINIT->HANDSHAKING->...->FLUSHING->SHUTDOWN chain plus a catch-all into CON_LOST). It is an internal invariant check: the event that caused the transition (data, EOF, or connection loss arriving in an unexpected order) does not fit the protocol's current phase.","triggerScenarios":"Out-of-order lifecycle events on an SSL connection: e.g. data_received or eof_received firing after the protocol already entered SHUTDOWN/CON_LOST, which usually stems from bugs in custom transports/loops replaying events, from misuse of the internal API, or from Python bugs in sslproto's state handling under aborted handshakes.","commonSituations":"Custom event loops or protocol wrappers re-delivering events; older CPython releases with known sslproto state-machine races (several were fixed across 3.6-3.12); force-closing connections during handshake combined with handshake timeouts; fuzz testing that sends EOF mid-handshake.","solutions":["Upgrade to the newest patch release of your Python version — multiple 'cannot switch state' races in sslproto have been fixed upstream.","If you wrap transports/protocols, ensure each event (connection_made, data_received, eof_received, connection_lost) is delivered at most once and in order per connection.","Avoid touching _SSLProtocol internals; use only public APIs (create_connection, start_tls, close()).","Capture a traceback when it occurs and check whether it follows an aborted handshake (handshake timeout + concurrent close) — restructure code to await handshake completion before closing."],"exampleFix":"// before: closing while handshake is still settling can race the state machine\nconn = await loop.create_connection(proto, 'h', 443, ssl=ctx)\nconn.close()  # immediate close during handshake window\n\n// after: let the handshake finish (or fail) before tearing down\nreader, writer = await asyncio.open_connection('h', 443, ssl=ctx,\n                                              ssl_handshake_timeout=10)\nawait writer.drain() if False else None\nwriter.close()\nawait writer.wait_closed()","handlingStrategy":"try-catch","validationCode":"# let the handshake settle before closing, avoiding shutdown races\nreader, writer = await asyncio.open_connection(\n    'host', 443, ssl=ctx, ssl_handshake_timeout=10.0)\n# ... use connection ...\nwriter.close()\nawait writer.wait_closed()","typeGuard":null,"tryCatchPattern":"try:\n    reader, writer = await asyncio.open_connection('host', 443, ssl=ctx)\nexcept RuntimeError as e:\n    if 'cannot switch state' in str(e):\n        log.exception('SSL state machine inconsistency; retrying once on a new connection')\n        reader, writer = await asyncio.open_connection('host', 443, ssl=ctx)\n    else:\n        raise","preventionTips":["Keep Python patched; several sslproto state-machine races were fixed across releases.","Avoid closing connections concurrently with an in-flight handshake; await the handshake first.","If you wrap transports, guarantee each lifecycle event fires once and in order."],"tags":["asyncio","ssl","state-machine","internal-api","lifecycle"],"backgroundTag":null,"analyzedSha":"bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6","analyzedAt":"2026-08-14T22:01:13.976Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}