Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Lib/asyncio/base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 timeout error
raise TimeoutError('create_connection failed')
finally:
exceptions = None

Expand Down
31 changes: 31 additions & 0 deletions Lib/test/test_asyncio/test_base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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.
Loading