python/cpython · error · TypeError

transport {transport!r} is not supported by start_tls()

Error message

transport {transport!r} is not supported by start_tls()

What it means

Raised by BaseEventLoop.start_tls() when the transport passed in does not set the _start_tls_compatible attribute to True. start_tls() can only upgrade an existing plain TCP transport (from create_connection/create_server accept) to TLS; datagram transports, pipe/subprocess transports, and already-encrypted SSL transports cannot be upgraded.

Source

Thrown at Lib/asyncio/base_events.py:1348

                        server_side=False,
                        server_hostname=None,
                        ssl_handshake_timeout=None,
                        ssl_shutdown_timeout=None):
        """Upgrade transport to TLS.

        Return a new transport that *protocol* should start using
        immediately.
        """
        if ssl is None:
            raise RuntimeError('Python ssl module is not available')

        if not isinstance(sslcontext, ssl.SSLContext):
            raise TypeError(
                f'sslcontext is expected to be an instance of ssl.SSLContext, '
                f'got {sslcontext!r}')

        if not getattr(transport, '_start_tls_compatible', False):
            raise TypeError(
                f'transport {transport!r} is not supported by start_tls()')

        waiter = self.create_future()
        ssl_protocol = sslproto.SSLProtocol(
            self, protocol, sslcontext, waiter,
            server_side, server_hostname,
            ssl_handshake_timeout=ssl_handshake_timeout,
            ssl_shutdown_timeout=ssl_shutdown_timeout,
            call_connection_made=False)

        # Pause early so that "ssl_protocol.data_received()" doesn't
        # have a chance to get called before "ssl_protocol.connection_made()".
        transport.pause_reading()

        # gh-142352: move buffered StreamReader data to SSLProtocol
        if server_side:
            from .streams import StreamReaderProtocol
            if isinstance(protocol, StreamReaderProtocol):

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Verify the transport came from loop.create_connection() or an accepted TCP server connection before calling start_tls()
  2. If the connection is already TLS, do not call start_tls() again; use the existing SSL transport or reconnect with ssl= passed to create_connection()
  3. For custom transports, set _start_tls_compatible = True only if the transport is a plain socket stream compatible with the selector implementation
  4. Check the protocol stack: unwrapping TLS (transport.get_extra_info('ssl_object') / unwrap) then re-wrapping is not supported by start_tls()

Example fix

// before
transport, protocol = await loop.create_datagram_endpoint(...)  # UDP
await loop.start_tls(transport, protocol, ctx)  # TypeError

// after
transport, protocol = await loop.create_connection(factory, host, port)  # TCP
 tls_transport = await loop.start_tls(transport, protocol, ctx)
Defensive patterns

Strategy: validation

Validate before calling

def can_start_tls(transport) -> bool:
    return bool(getattr(transport, '_start_tls_compatible', False))

if not can_start_tls(transport):
    raise RuntimeError(f'cannot upgrade {transport!r}; reconnect with ssl= instead')
tls_transport = await loop.start_tls(transport, protocol, ctx)

Type guard

def is_start_tls_compatible(t: asyncio.Transport) -> TypeGuard[asyncio.Transport]:
    return getattr(t, '_start_tls_compatible', False) is True

Try / catch

try:
    tls_t = await loop.start_tls(transport, protocol, ctx)
except TypeError as e:
    if 'not supported by start_tls' in str(e):
        transport.close()  # fall back: reconnect with ssl= from the start
    else:
        raise

Prevention

When it happens

Trigger: Calling loop.start_tls(transport, protocol, sslcontext) where transport is a datagram transport (create_datagram_endpoint), a _SelectorTransport pipe/stdin/stdout transport, an already-wrapped SSL transport, or a custom transport class that does not define _start_tls_compatible = True.

Common situations: Attempting STARTTLS-style upgrades (e.g. smtp, redis TLS upgrade) on a connection that is already TLS, or on a UDP-based protocol; passing the raw transport obtained from a transport/protocol pair that is not a socket stream transport; custom transport subclasses that forget the compatibility flag.

Related errors


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