{"record":{"id":"4bf9597090e9bfd1","repo":"python/cpython","slug":"creating-sslprotocoltransport-twice","errorCode":null,"errorMessage":"Creating _SSLProtocolTransport twice","messagePattern":"Creating _SSLProtocolTransport twice","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"Lib/asyncio/sslproto.py","lineNumber":377,"sourceCode":"            self._app_protocol_buffer_updated = app_protocol.buffer_updated\n            self._app_protocol_is_buffer = True\n        else:\n            self._app_protocol_is_buffer = False\n\n    def _wakeup_waiter(self, exc=None):\n        if self._waiter is None:\n            return\n        if not self._waiter.cancelled():\n            if exc is not None:\n                self._waiter.set_exception(exc)\n            else:\n                self._waiter.set_result(None)\n        self._waiter = None\n\n    def _get_app_transport(self):\n        if self._app_transport is None:\n            if self._app_transport_created:\n                raise RuntimeError('Creating _SSLProtocolTransport twice')\n            self._app_transport = _SSLProtocolTransport(self._loop, self)\n            self._app_transport_created = True\n        return self._app_transport\n\n    def _is_transport_closing(self):\n        return self._transport is not None and self._transport.is_closing()\n\n    def connection_made(self, transport):\n        \"\"\"Called when the low-level connection is made.\n\n        Start the SSL handshake.\n        \"\"\"\n        self._transport = transport\n        self._start_handshake()\n\n    def connection_lost(self, exc):\n        \"\"\"Called when the low-level connection is lost or closed.\n","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/python/cpython/blob/bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6/Lib/asyncio/sslproto.py#L359-L395","documentation":"Raised by _SSLProtocol._get_app_transport() as RuntimeError when a new _SSLProtocolTransport is requested after one was already created and released (i.e. _app_transport is None because it was closed/invalidated, but _app_transport_created is still True). The SSL protocol hands out exactly one application-level transport over its lifetime; asking for a second one indicates the protocol object is being driven after its transport was already consumed.","triggerScenarios":"Internal/library-level misuse: calling start_tls() machinery or _SSLProtocol lifecycle methods after the previous app transport was closed — e.g. performing a TLS upgrade twice on the same underlying protocol object, or an event loop/protocol wrapper re-triggering connection_made after connection_lost. Not reachable through the documented public transport API.","commonSituations":"Bugs in custom event loops or protocol wrappers that replay connection events; double start_tls upgrades on one connection; older Python versions with lifecycle races during aborted handshakes; third-party libraries (uvloop-era ported code) poking at _SSLProtocol internals.","solutions":["Audit for double TLS upgrades: call start_tls()/upgrade at most once per connection; create a fresh connection for a second TLS layer.","Ensure you never use the SSL transport or protocol object after connection_lost/close — drop all references in your protocol's connection_lost().","Upgrade Python to the latest patch release — several sslproto lifecycle races have been fixed over time.","If you maintain a custom loop/transport wrapper, verify connection_made/_get_app_transport are invoked exactly once per _SSLProtocol instance."],"exampleFix":"// before: double upgrade on one connection\ntls_transport = await loop.start_tls(raw_transport, protocol, ctx)\ntls_transport2 = await loop.start_tls(tls_transport, protocol, ctx2)\n# re-driving the same protocol can hit 'Creating _SSLProtocolTransport twice'\n\n// after: single TLS layer per connection; reconnect for another layer\ntls_transport = await loop.start_tls(raw_transport, protocol, ctx)\n# need another cert context -> open a new connection with ssl=ctx2","handlingStrategy":"validation","validationCode":"# enforce one TLS upgrade per connection at the application layer\nupgraded = set()\n\nasync def upgrade_once(conn_id, transport, protocol, ctx):\n    if conn_id in upgraded:\n        raise RuntimeError(f'connection {conn_id} already TLS-upgraded')\n    upgraded.add(conn_id)\n    return await loop.start_tls(transport, protocol, ctx)","typeGuard":null,"tryCatchPattern":"try:\n    tls_transport = await loop.start_tls(transport, protocol, ctx)\nexcept RuntimeError as e:\n    if '_SSLProtocolTransport twice' in str(e):\n        log.error('protocol already consumed by a previous TLS layer; reconnecting')\n        tls_transport = None  # signal caller to open a fresh connection\n    else:\n        raise","preventionTips":["Call start_tls() at most once per connection; open a new connection for further layers.","Drop references to transports/protocols in connection_lost so they cannot be reused.","Stay on public asyncio APIs; do not drive _SSLProtocol lifecycle methods yourself."],"tags":["asyncio","ssl","internal-api","lifecycle","start-tls"],"backgroundTag":null,"analyzedSha":"bc6749cc3b5ae4a5e88a6cc2d5b3bebbe354eae6","analyzedAt":"2026-08-14T22:01:13.976Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}