From ad7e39c5f5d655ead4ab9547d7bf8ac9fccc67c2 Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Mon, 23 Jun 2025 18:56:06 +0800 Subject: [PATCH 01/12] Fix IndexError in asyncio.create_connection with empty exceptions list Fix IndexError that occurs in asyncio.create_connection() when Happy Eyeballs algorithm leaves empty sublists in the exceptions list, which after flattening becomes an empty list causing str(exceptions[0]) to crash. The fix adds explicit handling for empty exceptions list by changing 'else:' to 'elif len(exceptions) > 1:' and adding a new else clause that raises OSError('create_connection failed') instead of crashing. --- Lib/asyncio/base_events.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 04fb961e9985e4..626dbeb0598d4a 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1161,7 +1161,7 @@ async def create_connection( raise ExceptionGroup("create_connection failed", exceptions) if len(exceptions) == 1: raise exceptions[0] - else: + elif len(exceptions) > 1: # If they all have the same str(), raise one. model = str(exceptions[0]) if all(str(exc) == model for exc in exceptions): @@ -1170,6 +1170,9 @@ async def create_connection( # the various error messages. raise OSError('Multiple exceptions: {}'.format( ', '.join(str(exc) for exc in exceptions))) + else: + # No exceptions were collected, raise a generic connection error + raise OSError('create_connection failed') finally: exceptions = None From a40961c37cfa28d09f4a44f6599c4719c501337c Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:04:26 +0000 Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst diff --git a/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst b/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst new file mode 100644 index 00000000000000..edbf0dd7da10c4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst @@ -0,0 +1 @@ +Fixed :exc:`IndexError` in :func:`asyncio.create_connection` that could occur when the Happy Eyeballs algorithm resulted in an empty exceptions list during concurrent connection attempts, particularly in high-load environments with mixed IPv4/IPv6 connectivity. From f432b3f6d27dd522955e9d0f180ab60b932c7221 Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Mon, 23 Jun 2025 19:23:20 +0800 Subject: [PATCH 03/12] Fix NEWS entry for gh-135836 - correct function reference Changed :func: to double backticks to fix Sphinx reference error --- .../next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst b/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst index edbf0dd7da10c4..1866dfa1bd42ee 100644 --- a/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst +++ b/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst @@ -1 +1 @@ -Fixed :exc:`IndexError` in :func:`asyncio.create_connection` that could occur when the Happy Eyeballs algorithm resulted in an empty exceptions list during concurrent connection attempts, particularly in high-load environments with mixed IPv4/IPv6 connectivity. +Fixed :exc:`IndexError` in ``asyncio.create_connection()`` that could occur when the Happy Eyeballs algorithm resulted in an empty exceptions list during connection attempts. From f37d9e3a0e28855e343e2b64df9ce1d2c6ec6305 Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Mon, 23 Jun 2025 19:51:20 +0800 Subject: [PATCH 04/12] Re-trigger CI From 40d4551de52b0b2cdbed0ff07ed66ac39722485d Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Mon, 23 Jun 2025 21:07:43 +0800 Subject: [PATCH 05/12] Address core dev feedback: use TimeoutError instead of OSError Core developer suggested TimeoutError is more appropriate when empty exceptions list indicates all connections exceeded their timeout. --- Lib/asyncio/base_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 626dbeb0598d4a..d1fd3c6a9526d2 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1171,8 +1171,8 @@ async def create_connection( raise OSError('Multiple exceptions: {}'.format( ', '.join(str(exc) for exc in exceptions))) else: - # No exceptions were collected, raise a generic connection error - raise OSError('create_connection failed') + # No exceptions were collected, raise a timeout error + raise TimeoutError('create_connection failed') finally: exceptions = None From 0e2fb84762f1cdfbec1ddef66b958dc085b233a6 Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Mon, 30 Jun 2025 02:28:48 +0800 Subject: [PATCH 06/12] Add test asyncio.create_connection Happy Eyeballs empty exceptions and update NEWS entry. --- Lib/test/test_asyncio/test_base_events.py | 31 +++++++++++++++++++ ...-06-23-11-04-25.gh-issue-135836.-C-c4v.rst | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 2ca5c4c6719c41..2be0bc2bd248c4 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1190,6 +1190,37 @@ def getaddrinfo(*args, **kw): self.loop.run_until_complete(coro) self.assertTrue(sock.close.called) + @patch_socket + def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): + # Test for gh-135836: Fix IndexError when Happy Eyeballs algorithm + # results in empty exceptions list + from unittest import mock + + async def getaddrinfo(*args, **kw): + return [(socket.AF_INET, socket.SOCK_STREAM, 0, '', ('127.0.0.1', 80)), + (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('::1', 80))] + + def getaddrinfo_task(*args, **kwds): + return self.loop.create_task(getaddrinfo(*args, **kwds)) + + self.loop.getaddrinfo = getaddrinfo_task + + # Mock staggered_race to return empty exceptions list + # This simulates the scenario where Happy Eyeballs algorithm + # cancels all attempts but doesn't properly collect exceptions + with mock.patch('asyncio.staggered.staggered_race') as mock_staggered: + # Return (None, []) - no winner, empty exceptions list + async def mock_race(coro_fns, delay, loop): + return None, [] + mock_staggered.side_effect = mock_race + + coro = self.loop.create_connection( + MyProto, 'example.com', 80, happy_eyeballs_delay=0.1) + + # Should raise TimeoutError instead of IndexError + with self.assertRaises(TimeoutError): + self.loop.run_until_complete(coro) + def test_create_connection_host_port_sock(self): coro = self.loop.create_connection( MyProto, 'example.com', 80, sock=object()) diff --git a/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst b/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst index 1866dfa1bd42ee..f93c9faee5863a 100644 --- a/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst +++ b/Misc/NEWS.d/next/Library/2025-06-23-11-04-25.gh-issue-135836.-C-c4v.rst @@ -1 +1 @@ -Fixed :exc:`IndexError` in ``asyncio.create_connection()`` that could occur when the Happy Eyeballs algorithm resulted in an empty exceptions list during connection attempts. +Fix :exc:`IndexError` in :meth:`asyncio.loop.create_connection` that could occur when the Happy Eyeballs algorithm resulted in an empty exceptions list during connection attempts. From f334cf1ae2a14aad1561cf60a02893c8530f0d1e Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Mon, 30 Jun 2025 02:31:08 +0800 Subject: [PATCH 07/12] Test asyncio.create_connection Happy Eyeballs empty exceptions and update NEWS entry. --- Lib/test/test_asyncio/test_base_events.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 2be0bc2bd248c4..594a1200477261 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1195,16 +1195,16 @@ def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): # Test for gh-135836: Fix IndexError when Happy Eyeballs algorithm # results in empty exceptions list from unittest import mock - + async def getaddrinfo(*args, **kw): return [(socket.AF_INET, socket.SOCK_STREAM, 0, '', ('127.0.0.1', 80)), (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('::1', 80))] - + def getaddrinfo_task(*args, **kwds): return self.loop.create_task(getaddrinfo(*args, **kwds)) - + self.loop.getaddrinfo = getaddrinfo_task - + # Mock staggered_race to return empty exceptions list # This simulates the scenario where Happy Eyeballs algorithm # cancels all attempts but doesn't properly collect exceptions @@ -1213,10 +1213,10 @@ def getaddrinfo_task(*args, **kwds): async def mock_race(coro_fns, delay, loop): return None, [] mock_staggered.side_effect = mock_race - + coro = self.loop.create_connection( MyProto, 'example.com', 80, happy_eyeballs_delay=0.1) - + # Should raise TimeoutError instead of IndexError with self.assertRaises(TimeoutError): self.loop.run_until_complete(coro) From cb817c15b7fecb8734c0d5122b225e62c026f2af Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Tue, 1 Jul 2025 18:59:34 +0800 Subject: [PATCH 08/12] test: check for error message in test_create_connection_happy_eyeballs_empty_exceptions --- Lib/test/test_asyncio/test_base_events.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 594a1200477261..629fe02383ed46 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1191,14 +1191,17 @@ def getaddrinfo(*args, **kw): self.assertTrue(sock.close.called) @patch_socket - def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): + async def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): # Test for gh-135836: Fix IndexError when Happy Eyeballs algorithm # results in empty exceptions list - from unittest import mock async def getaddrinfo(*args, **kw): - return [(socket.AF_INET, socket.SOCK_STREAM, 0, '', ('127.0.0.1', 80)), - (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('::1', 80))] + return [ + (socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, + '', ('127.0.0.1', 80)), + (socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_TCP, + '', ('::1', 80)), + ] def getaddrinfo_task(*args, **kwds): return self.loop.create_task(getaddrinfo(*args, **kwds)) @@ -1218,12 +1221,13 @@ async def mock_race(coro_fns, delay, loop): MyProto, 'example.com', 80, happy_eyeballs_delay=0.1) # Should raise TimeoutError instead of IndexError - with self.assertRaises(TimeoutError): - self.loop.run_until_complete(coro) + with self.assertRaisesRegex(TimeoutError, "connection timed out"): + await coro def test_create_connection_host_port_sock(self): + # host, port and sock are specified coro = self.loop.create_connection( - MyProto, 'example.com', 80, sock=object()) + MyProto, 'example.com', 80, sock=mock.Mock()) self.assertRaises(ValueError, self.loop.run_until_complete, coro) def test_create_connection_wrong_sock(self): From 45da4d2fbc208cfac9003747508f19bd44c80f8f Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Tue, 1 Jul 2025 19:09:44 +0800 Subject: [PATCH 09/12] test: fix test_create_connection_happy_eyeballs_empty_exceptions to be synchronous and check correct error message --- Lib/test/test_asyncio/test_base_events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 629fe02383ed46..de82b32c9ffdae 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1191,7 +1191,7 @@ def getaddrinfo(*args, **kw): self.assertTrue(sock.close.called) @patch_socket - async def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): + def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): # Test for gh-135836: Fix IndexError when Happy Eyeballs algorithm # results in empty exceptions list @@ -1221,8 +1221,8 @@ async def mock_race(coro_fns, delay, loop): MyProto, 'example.com', 80, happy_eyeballs_delay=0.1) # Should raise TimeoutError instead of IndexError - with self.assertRaisesRegex(TimeoutError, "connection timed out"): - await coro + with self.assertRaisesRegex(TimeoutError, "create_connection failed"): + self.loop.run_until_complete(coro) def test_create_connection_host_port_sock(self): # host, port and sock are specified From 99dffd93261593148b6875e19ce979fcdf03a2be Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Tue, 1 Jul 2025 19:19:58 +0800 Subject: [PATCH 10/12] Revert unintended change --- Lib/test/test_asyncio/test_base_events.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index de82b32c9ffdae..4930e3a975054c 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1196,12 +1196,8 @@ def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): # results in empty exceptions list async def getaddrinfo(*args, **kw): - return [ - (socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, - '', ('127.0.0.1', 80)), - (socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_TCP, - '', ('::1', 80)), - ] + return [(socket.AF_INET, socket.SOCK_STREAM, 0, '', ('127.0.0.1', 80)), + (socket.AF_INET6, socket.SOCK_STREAM, 0, '', ('::1', 80))] def getaddrinfo_task(*args, **kwds): return self.loop.create_task(getaddrinfo(*args, **kwds)) @@ -1225,9 +1221,8 @@ async def mock_race(coro_fns, delay, loop): self.loop.run_until_complete(coro) def test_create_connection_host_port_sock(self): - # host, port and sock are specified coro = self.loop.create_connection( - MyProto, 'example.com', 80, sock=mock.Mock()) + MyProto, 'example.com', 80, sock=object()) self.assertRaises(ValueError, self.loop.run_until_complete, coro) def test_create_connection_wrong_sock(self): From 0fb0fcb6b23febdd9ae074cf109d575835196528 Mon Sep 17 00:00:00 2001 From: heliang666s <147408835+heliang666s@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:21:37 +0800 Subject: [PATCH 11/12] Update Lib/asyncio/base_events.py Co-authored-by: Kumar Aditya --- Lib/asyncio/base_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index d1fd3c6a9526d2..2ff9e4017bb245 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1161,7 +1161,7 @@ async def create_connection( raise ExceptionGroup("create_connection failed", exceptions) if len(exceptions) == 1: raise exceptions[0] - elif len(exceptions) > 1: + elif exceptions: # If they all have the same str(), raise one. model = str(exceptions[0]) if all(str(exc) == model for exc in exceptions): From 718124409561836743538d67225f8eba21c63716 Mon Sep 17 00:00:00 2001 From: heliang666s <147408835+heliang666s@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:22:48 +0800 Subject: [PATCH 12/12] Update Lib/test/test_asyncio/test_base_events.py Co-authored-by: Kumar Aditya --- Lib/test/test_asyncio/test_base_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 4930e3a975054c..bb9f366fc411aa 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1192,7 +1192,7 @@ def getaddrinfo(*args, **kw): @patch_socket def test_create_connection_happy_eyeballs_empty_exceptions(self, m_socket): - # Test for gh-135836: Fix IndexError when Happy Eyeballs algorithm + # See gh-135836: Fix IndexError when Happy Eyeballs algorithm # results in empty exceptions list async def getaddrinfo(*args, **kw):