Skip to content

start_tls() returns a closed transport instead of raising when the client disconnects before the handshake (for eager tasks) #784

Open

Description

@chrisjbillington

Arch Linux, Python 3.14.8, uvloop 0.23.0

When using an eager task factory, if a client closes the connection before the TLS handshake, the server's StreamWriter.start_tls returns normally with a closed transport, and get_extra_info('socket') is None.

With plain asyncio, or with uvloop without eager tasks, start_tls raises ConnectionResetError, so uvloop should presumably do the same.

Here's a reproduction script for (disclosure, AI-generated, but I've run it and it does demonstrate the issue). This is for Python 3.13+ as it requires PSK functions in the ssl module, but the issue also occurs on Python 3.12 using a certificate instead of a PSK, so it's not PSK-specific.

(note this reproducer also contains a workaround for #779, otherwise one can't actually enable eager tasks at all)

import asyncio
import socket
import ssl
import sys

import uvloop


def eager_task_factory(loop, coro, *, eager_start=None, **kwargs):
    # uvloop passes eager_start=None, which asyncio.eager_task_factory treats as False
    if sys.version_info >= (3, 14):
        return asyncio.Task(coro, loop=loop, eager_start=True, **kwargs)
    return asyncio.eager_task_factory(loop, coro, **kwargs)


async def main(eager):
    if eager:
        asyncio.get_running_loop().set_task_factory(eager_task_factory)
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    context.minimum_version = ssl.TLSVersion.TLSv1_3
    context.set_psk_server_callback(lambda identity: b"k" * 32)
    result = asyncio.get_running_loop().create_future()

    async def on_connection(reader, writer):
        try:
            await writer.start_tls(context)
        except Exception as e:
            result.set_result(f"start_tls raised {type(e).__name__}")
        else:
            result.set_result(
                f"start_tls returned, is_closing() is {writer.is_closing()}, "
                f"get_extra_info('socket') is {writer.get_extra_info('socket')}"
            )

    server = await asyncio.start_server(on_connection, "127.0.0.1", 0)
    # The client connects and closes the connection without sending anything:
    socket.create_connection(server.sockets[0].getsockname()).close()
    print(await asyncio.wait_for(result, 5))
    server.close()


print(f"Python {sys.version.split()[0]}, uvloop {uvloop.__version__}")
for name, loop_factory in [
    ("asyncio", asyncio.new_event_loop),
    ("uvloop", uvloop.new_event_loop),
]:
    for eager in [False, True]:
        print(f"{name}, eager tasks {eager}: ", end="")
        asyncio.run(main(eager), loop_factory=loop_factory)

On my system:

Python 3.14.8, uvloop 0.23.0
asyncio, eager tasks False: start_tls raised ConnectionResetError
asyncio, eager tasks True: start_tls raised ConnectionResetError
uvloop, eager tasks False: start_tls raised ConnectionResetError
uvloop, eager tasks True: start_tls returned, is_closing() is True, get_extra_info('socket') is None

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions