Skip to content

asyncio.loop.create_connection closes user-provided socket on transport creation failure (Regression in 3.14.7) #155305

Description

@syphyr

Bug report

Bug description:

When asyncio.loop.create_connection is called with a sock argument, the provided socket is now closed if the transport cannot be created (due to the fix for GH-153133). This causes regressions in libraries like yt-dlp and websockets where the socket is expected to remain open for error handling or graceful shutdown.

The socket should only be closed if it was created internally by asyncio.

Suggested Fix:
Pass a flag sock_was_provided to _create_connection_transport and only call sock.close() if sock_was_provided is False.

diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py
index a2a840fa081..5fe925e3c15 100644
--- a/Lib/asyncio/base_events.py
+++ b/Lib/asyncio/base_events.py
@@ -1078,6 +1078,7 @@ async def create_connection(
         connection in the background.  When successful, the coroutine
         returns a (transport, protocol) pair.
         """
+        sock_was_provided = sock is not None
         if server_hostname is not None and not ssl:
             raise ValueError('server_hostname is only meaningful with ssl')
 
@@ -1199,7 +1200,8 @@ async def create_connection(
         transport, protocol = await self._create_connection_transport(
             sock, protocol_factory, ssl, server_hostname,
             ssl_handshake_timeout=ssl_handshake_timeout,
-            ssl_shutdown_timeout=ssl_shutdown_timeout)
+            ssl_shutdown_timeout=ssl_shutdown_timeout,
+            sock_was_provided=sock_was_provided)
         if self._debug:
             # Get the socket from the transport because SSL transport closes
             # the old socket and creates a new SSL socket
@@ -1212,7 +1214,8 @@ async def _create_connection_transport(
             self, sock, protocol_factory, ssl,
             server_hostname, server_side=False,
             ssl_handshake_timeout=None,
-            ssl_shutdown_timeout=None):
+            ssl_shutdown_timeout=None,
+            sock_was_provided=False):
 
         try:
             sock.setblocking(False)
@@ -1229,8 +1232,10 @@ async def _create_connection_transport(
             else:
                 transport = self._make_socket_transport(sock, protocol, waiter)
         except:
-            # gh-153133: close the socket if the transport is never created.
-            sock.close()
+            # gh-153133: close the socket if the transport is never created,
+            # unless it was provided by the user.
+            if not sock_was_provided:
+                sock.close()
             raise
 
         try:
@@ -1695,7 +1700,8 @@ async def connect_accepted_socket(
         transport, protocol = await self._create_connection_transport(
             sock, protocol_factory, ssl, '', server_side=True,
             ssl_handshake_timeout=ssl_handshake_timeout,
-            ssl_shutdown_timeout=ssl_shutdown_timeout)
+            ssl_shutdown_timeout=ssl_shutdown_timeout,
+            sock_was_provided=True)
         if self._debug:
             # Get the socket from the transport because SSL transport closes
             # the old socket and creates a new SSL socket

CPython versions tested on:

3.14

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions