Skip to content

gh-74166: Add option to get all errors from socket.create_connection #91586

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 10 additions & 1 deletion Doc/library/socket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ The following functions all create :ref:`socket objects <socket-objects>`.
Windows support added.


.. function:: create_connection(address[, timeout[, source_address]])
.. function:: create_connection(address[, timeout[, source_address[, all_errors]]])

Connect to a TCP service listening on the internet *address* (a 2-tuple
``(host, port)``), and return the socket object. This is a higher-level
Expand All @@ -679,9 +679,18 @@ The following functions all create :ref:`socket objects <socket-objects>`.
socket to bind to as its source address before connecting. If host or port
are '' or 0 respectively the OS default behavior will be used.

When a connection cannot be created, an exception is raised. By default,
it is the exception from the last address in the list. If *all_errors*
is ``True``, it is an :exc:`ExceptionGroup` containing the errors of all
attempts.

.. versionchanged:: 3.2
*source_address* was added.

.. versionchanged:: 3.11
*all_errors* was added.


.. function:: create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, dualstack_ipv6=False)

Convenience function which creates a TCP socket bound to *address* (a 2-tuple
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.11.rst
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ socket
* Add CAN Socket support for NetBSD.
(Contributed by Thomas Klausner in :issue:`30512`.)

* :meth:`~socket.create_connection` has an option to raise, in case of
failure to connect, an :exc:`ExceptionGroup` containing all errors
instead of only raising the last error.
(Contributed by Irit Katriel in :issue:`29980`).

sqlite3
-------
Expand Down
24 changes: 15 additions & 9 deletions Lib/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ def getfqdn(name=''):
_GLOBAL_DEFAULT_TIMEOUT = object()

def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
source_address=None):
source_address=None, all_errors=False):
"""Connect to *address* and return the socket object.

Convenience function. Connect to *address* (a 2-tuple ``(host,
Expand All @@ -816,11 +816,13 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
global default timeout setting returned by :func:`getdefaulttimeout`
is used. If *source_address* is set it must be a tuple of (host, port)
for the socket to bind as a source address before making the connection.
A host of '' or port 0 tells the OS to use the default.
A host of '' or port 0 tells the OS to use the default. When a connection
cannot be created, raises the last error if *all_errors* is False,
and an ExceptionGroup of all errors if *all_errors* is True.
"""

host, port = address
err = None
exceptions = []
for res in getaddrinfo(host, port, 0, SOCK_STREAM):
af, socktype, proto, canonname, sa = res
sock = None
Expand All @@ -832,20 +834,24 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
err = None
exceptions.clear()
return sock

except error as _:
err = _
except error as exc:
if not all_errors:
exceptions.clear() # raise only the last error
exceptions.append(exc)
if sock is not None:
sock.close()

if err is not None:
if len(exceptions):
try:
raise err
if not all_errors:
raise exceptions[0]
raise ExceptionGroup("create_connection failed", exceptions)
finally:
# Break explicitly a reference cycle
err = None
exceptions = None
else:
raise error("getaddrinfo returns an empty list")

Expand Down
18 changes: 18 additions & 0 deletions Lib/test/test_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -5177,6 +5177,24 @@ def test_create_connection(self):
expected_errnos = socket_helper.get_socket_conn_refused_errs()
self.assertIn(cm.exception.errno, expected_errnos)

def test_create_connection_all_errors(self):
port = socket_helper.find_unused_port()
try:
socket.create_connection((HOST, port), all_errors=True)
except ExceptionGroup as e:
eg = e
else:
self.fail('expected connection to fail')

self.assertIsInstance(eg, ExceptionGroup)
for e in eg.exceptions:
self.assertIsInstance(e, OSError)

addresses = socket.getaddrinfo(
'localhost', port, 0, socket.SOCK_STREAM)
# assert that we got an exception for each address
self.assertEqual(len(addresses), len(eg.exceptions))

def test_create_connection_timeout(self):
# Issue #9792: create_connection() should not recast timeout errors
# as generic socket errors.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add option to raise all errors from :meth:`~socket.create_connection` in an :exc:`ExceptionGroup` when it fails to create a connection. The default remains to raise only the last error that had occurred when multiple addresses were tried.