python/cpython · error · RuntimeError

Creating _SSLProtocolTransport twice

Error message

Creating _SSLProtocolTransport twice

What it means

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.

Source

Thrown at Lib/asyncio/sslproto.py:377

            self._app_protocol_buffer_updated = app_protocol.buffer_updated
            self._app_protocol_is_buffer = True
        else:
            self._app_protocol_is_buffer = False

    def _wakeup_waiter(self, exc=None):
        if self._waiter is None:
            return
        if not self._waiter.cancelled():
            if exc is not None:
                self._waiter.set_exception(exc)
            else:
                self._waiter.set_result(None)
        self._waiter = None

    def _get_app_transport(self):
        if self._app_transport is None:
            if self._app_transport_created:
                raise RuntimeError('Creating _SSLProtocolTransport twice')
            self._app_transport = _SSLProtocolTransport(self._loop, self)
            self._app_transport_created = True
        return self._app_transport

    def _is_transport_closing(self):
        return self._transport is not None and self._transport.is_closing()

    def connection_made(self, transport):
        """Called when the low-level connection is made.

        Start the SSL handshake.
        """
        self._transport = transport
        self._start_handshake()

    def connection_lost(self, exc):
        """Called when the low-level connection is lost or closed.

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Audit for double TLS upgrades: call start_tls()/upgrade at most once per connection; create a fresh connection for a second TLS layer.
  2. Ensure you never use the SSL transport or protocol object after connection_lost/close — drop all references in your protocol's connection_lost().
  3. Upgrade Python to the latest patch release — several sslproto lifecycle races have been fixed over time.
  4. If you maintain a custom loop/transport wrapper, verify connection_made/_get_app_transport are invoked exactly once per _SSLProtocol instance.

Example fix

// before: double upgrade on one connection
tls_transport = await loop.start_tls(raw_transport, protocol, ctx)
tls_transport2 = await loop.start_tls(tls_transport, protocol, ctx2)
# re-driving the same protocol can hit 'Creating _SSLProtocolTransport twice'

// after: single TLS layer per connection; reconnect for another layer
tls_transport = await loop.start_tls(raw_transport, protocol, ctx)
# need another cert context -> open a new connection with ssl=ctx2
Defensive patterns

Strategy: validation

Validate before calling

# enforce one TLS upgrade per connection at the application layer
upgraded = set()

async def upgrade_once(conn_id, transport, protocol, ctx):
    if conn_id in upgraded:
        raise RuntimeError(f'connection {conn_id} already TLS-upgraded')
    upgraded.add(conn_id)
    return await loop.start_tls(transport, protocol, ctx)

Try / catch

try:
    tls_transport = await loop.start_tls(transport, protocol, ctx)
except RuntimeError as e:
    if '_SSLProtocolTransport twice' in str(e):
        log.error('protocol already consumed by a previous TLS layer; reconnecting')
        tls_transport = None  # signal caller to open a fresh connection
    else:
        raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/4bf9597090e9bfd1. Report an issue: GitHub.