diff --git a/Lib/socket.py b/Lib/socket.py index 9ed2de9132b..bd364e70db9 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -548,8 +548,8 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, An host of '' or port 0 tells the OS to use the default. """ - msg = "getaddrinfo returns an empty list" host, port = address + err = None for res in getaddrinfo(host, port, 0, SOCK_STREAM): af, socktype, proto, canonname, sa = res sock = None @@ -562,8 +562,12 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, sock.connect(sa) return sock - except error, msg: + except error as _: + err = _ if sock is not None: sock.close() - raise error, msg + if err is not None: + raise err + else: + raise error("getaddrinfo returns an empty list") diff --git a/Lib/test/test_robotparser.py b/Lib/test/test_robotparser.py index aa73ec5663d..3376a8a3cb4 100644 --- a/Lib/test/test_robotparser.py +++ b/Lib/test/test_robotparser.py @@ -232,23 +232,24 @@ class NetworkTestCase(unittest.TestCase): def testPasswordProtectedSite(self): test_support.requires('network') - # XXX it depends on an external resource which could be unavailable - url = 'http://mueblesmoraleda.com' - parser = robotparser.RobotFileParser() - parser.set_url(url) - try: - parser.read() - except IOError: - self.skipTest('%s is unavailable' % url) - self.assertEqual(parser.can_fetch("*", url+"/robots.txt"), False) + with test_support.transient_internet('mueblesmoraleda.com'): + url = 'http://mueblesmoraleda.com' + parser = robotparser.RobotFileParser() + parser.set_url(url) + try: + parser.read() + except IOError: + self.skipTest('%s is unavailable' % url) + self.assertEqual(parser.can_fetch("*", url+"/robots.txt"), False) def testPythonOrg(self): test_support.requires('network') - parser = robotparser.RobotFileParser( - "http://www.python.org/robots.txt") - parser.read() - self.assertTrue(parser.can_fetch("*", - "http://www.python.org/robots.txt")) + with test_support.transient_internet('www.python.org'): + parser = robotparser.RobotFileParser( + "http://www.python.org/robots.txt") + parser.read() + self.assertTrue( + parser.can_fetch("*", "http://www.python.org/robots.txt")) def test_main(): diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index da8e155b0e1..5b5d4fb237c 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -12,6 +12,7 @@ import Queue import sys import os import array +import contextlib from weakref import proxy import signal @@ -1048,12 +1049,42 @@ class BasicTCPTest2(NetworkConnectionTest, BasicTCPTest): """ class NetworkConnectionNoServer(unittest.TestCase): - def testWithoutServer(self): + class MockSocket(socket.socket): + def connect(self, *args): + raise socket.timeout('timed out') + + @contextlib.contextmanager + def mocked_socket_module(self): + """Return a socket which times out on connect""" + old_socket = socket.socket + socket.socket = self.MockSocket + try: + yield + finally: + socket.socket = old_socket + + def test_connect(self): port = test_support.find_unused_port() - self.assertRaises( - socket.error, - lambda: socket.create_connection((HOST, port)) - ) + cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + with self.assertRaises(socket.error) as cm: + cli.connect((HOST, port)) + self.assertEqual(cm.exception.errno, errno.ECONNREFUSED) + + def test_create_connection(self): + # Issue #9792: errors raised by create_connection() should have + # a proper errno attribute. + port = test_support.find_unused_port() + with self.assertRaises(socket.error) as cm: + socket.create_connection((HOST, port)) + self.assertEqual(cm.exception.errno, errno.ECONNREFUSED) + + def test_create_connection_timeout(self): + # Issue #9792: create_connection() should not recast timeout errors + # as generic socket errors. + with self.mocked_socket_module(): + with self.assertRaises(socket.timeout): + socket.create_connection((HOST, 1234)) + @unittest.skipUnless(thread, 'Threading required for this test.') class NetworkConnectionAttributesTest(SocketTCPTest, ThreadableTest): diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index c32ee04b381..2cdef1668ef 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -287,10 +287,10 @@ class NetworkedTests(unittest.TestCase): # NOTE: https://sha256.tbs-internet.com is another possible test host remote = ("sha2.hboeck.de", 443) sha256_cert = os.path.join(os.path.dirname(__file__), "sha256.pem") - s = ssl.wrap_socket(socket.socket(socket.AF_INET), - cert_reqs=ssl.CERT_REQUIRED, - ca_certs=sha256_cert,) - with test_support.transient_internet(): + with test_support.transient_internet("sha2.hboeck.de"): + s = ssl.wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_REQUIRED, + ca_certs=sha256_cert,) try: s.connect(remote) if test_support.verbose: diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index ce2b679ddd4..32ff970f7dc 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -750,32 +750,55 @@ class TransientResource(object): raise ResourceDenied("an optional resource is not available") -_transients = { - IOError: (errno.ECONNRESET, errno.ETIMEDOUT), - socket.error: (errno.ECONNRESET,), - socket.gaierror: [getattr(socket, t) - for t in ('EAI_NODATA', 'EAI_NONAME') - if hasattr(socket, t)], - } @contextlib.contextmanager -def transient_internet(): +def transient_internet(resource_name, timeout=30.0, errnos=()): """Return a context manager that raises ResourceDenied when various issues - with the Internet connection manifest themselves as exceptions. + with the Internet connection manifest themselves as exceptions.""" + default_errnos = [ + ('ECONNREFUSED', 111), + ('ECONNRESET', 104), + ('ENETUNREACH', 101), + ('ETIMEDOUT', 110), + ] - Errors caught: - timeout IOError errno = ETIMEDOUT - socket reset socket.error, IOError errno = ECONNRESET - dns no data socket.gaierror errno = EAI_NODATA - dns no name socket.gaierror errno = EAI_NONAME - """ + denied = ResourceDenied("Resource '%s' is not available" % resource_name) + captured_errnos = errnos + if not captured_errnos: + captured_errnos = [getattr(errno, name, num) + for (name, num) in default_errnos] + + def filter_error(err): + if (isinstance(err, socket.timeout) or + getattr(err, 'errno', None) in captured_errnos): + if not verbose: + sys.stderr.write(denied.args[0] + "\n") + raise denied + + old_timeout = socket.getdefaulttimeout() try: + if timeout is not None: + socket.setdefaulttimeout(timeout) yield - except tuple(_transients) as err: - for errtype in _transients: - if isinstance(err, errtype) and err.errno in _transients[errtype]: - raise ResourceDenied("could not establish network " - "connection ({})".format(err)) + except IOError as err: + # urllib can wrap original socket errors multiple times (!), we must + # unwrap to get at the original error. + while True: + a = err.args + if len(a) >= 1 and isinstance(a[0], IOError): + err = a[0] + # The error can also be wrapped as args[1]: + # except socket.error as msg: + # raise IOError('socket error', msg).with_traceback(sys.exc_info()[2]) + elif len(a) >= 2 and isinstance(a[1], IOError): + err = a[1] + else: + break + filter_error(err) raise + # XXX should we catch generic exceptions and look for their + # __cause__ or __context__? + finally: + socket.setdefaulttimeout(old_timeout) @contextlib.contextmanager diff --git a/Lib/test/test_urllib2net.py b/Lib/test/test_urllib2net.py index 4da38eff24d..76572b052b7 100644 --- a/Lib/test/test_urllib2net.py +++ b/Lib/test/test_urllib2net.py @@ -192,7 +192,7 @@ class OtherNetworkTests(unittest.TestCase): raise else: try: - with test_support.transient_internet(): + with test_support.transient_internet(url): buf = f.read() debug("read %d bytes" % len(buf)) except socket.timeout: diff --git a/Misc/NEWS b/Misc/NEWS index 2b5562cd976..16304995e2b 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -36,6 +36,11 @@ Core and Builtins Library ------- +- Issue #9792: In case of connection failure, socket.create_connection() + would swallow the exception and raise a new one, making it impossible + to fetch the original errno, or to filter timeout errors. Now the + original error is re-raised. + - Issue #9758: When fcntl.ioctl() was called with mutable_flag set to True, and the passed buffer was exactly 1024 bytes long, the buffer wouldn't be updated back after the system call. Original patch by Brian Brazil.