Skip to content

Commit 6849c59

Browse files
committed
pythongh-74166: Add option to get all errors from socket.create_connection
1 parent bd26ef5 commit 6849c59

File tree

4 files changed

+46
-9
lines changed

4 files changed

+46
-9
lines changed

Doc/library/socket.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ The following functions all create :ref:`socket objects <socket-objects>`.
660660
Windows support added.
661661

662662

663-
.. function:: create_connection(address[, timeout[, source_address]])
663+
.. function:: create_connection(address[, timeout[, source_address[, all_errors]]])
664664

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

682+
When a connection cannot be created, an exception is raised. By default,
683+
it is the exception from the last address in the list. If *all_errors*
684+
is ``True``, it is an :exc:`ExceptionGroup` containing the errors of all
685+
attempts.
686+
682687
.. versionchanged:: 3.2
683688
*source_address* was added.
684689

690+
.. versionchanged:: 3.11
691+
*all_errors* was added.
692+
693+
685694
.. function:: create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, dualstack_ipv6=False)
686695

687696
Convenience function which creates a TCP socket bound to *address* (a 2-tuple

Doc/whatsnew/3.11.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ socket
346346
* Add CAN Socket support for NetBSD.
347347
(Contributed by Thomas Klausner in :issue:`30512`.)
348348

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

350354
sqlite3
351355
-------

Lib/socket.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ def getfqdn(name=''):
806806
_GLOBAL_DEFAULT_TIMEOUT = object()
807807

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

822824
host, port = address
823-
err = None
825+
exceptions = []
824826
for res in getaddrinfo(host, port, 0, SOCK_STREAM):
825827
af, socktype, proto, canonname, sa = res
826828
sock = None
@@ -835,17 +837,21 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
835837
err = None
836838
return sock
837839

838-
except error as _:
839-
err = _
840+
except error as exc:
841+
if not all_errors:
842+
exceptions.clear() # raise only the last error
843+
exceptions.append(exc)
840844
if sock is not None:
841845
sock.close()
842846

843-
if err is not None:
847+
if len(exceptions):
844848
try:
845-
raise err
849+
if not all_errors:
850+
raise exceptions[0]
851+
raise ExceptionGroup("create_connection failed", exceptions)
846852
finally:
847853
# Break explicitly a reference cycle
848-
err = None
854+
exceptions = None
849855
else:
850856
raise error("getaddrinfo returns an empty list")
851857

Lib/test/test_socket.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5177,6 +5177,24 @@ def test_create_connection(self):
51775177
expected_errnos = socket_helper.get_socket_conn_refused_errs()
51785178
self.assertIn(cm.exception.errno, expected_errnos)
51795179

5180+
def test_create_connection_all_errors(self):
5181+
port = socket_helper.find_unused_port()
5182+
try:
5183+
socket.create_connection((HOST, port), all_errors=True)
5184+
except* OSError as e:
5185+
eg = e
5186+
else:
5187+
self.fail('expected connection to fail')
5188+
5189+
self.assertIsInstance(eg, ExceptionGroup)
5190+
for e in eg.exceptions:
5191+
self.assertIsInstance(e, OSError)
5192+
5193+
addresses = socket.getaddrinfo(
5194+
'localhost', port, 0, socket.SOCK_STREAM)
5195+
# assert that we got an exception for each address
5196+
self.assertEqual(len(addresses), len(eg.exceptions))
5197+
51805198
def test_create_connection_timeout(self):
51815199
# Issue #9792: create_connection() should not recast timeout errors
51825200
# as generic socket errors.

0 commit comments

Comments
 (0)