Repository: urllib3/urllib3

Issue:
Defensively close connections
Generally speaking, if we hit an exception when working with a connection we should assume that the connection is dead. We've not been defensive enough with this in the past, and that attitude pretty much just leads us to a constant cycle of bug-squashing each time we discover a new connection-invalidating exception.

In this case, the problem (and the fix) were inspired by [this HN post](https://news.ycombinator.com/item?id=10512343). For that reason, I'd like to credit @rkday (in his guise as @rkd-msw) for the actual fix. Rob, if you'd like to, I'm happy for you to open a new pull request with these commits in place. If you don't care about the commit log, I've made sure you're added to CONTRIBUTORS.



Discussion:

Resolving patch:
diff --git a/urllib3/response.py b/urllib3/response.py
--- a/urllib3/response.py
+++ b/urllib3/response.py
@@ -236,6 +236,12 @@ def _error_catcher(self):
             if self._original_response and not self._original_response.isclosed():
                 self._original_response.close()
 
+            # Closing the response may not actually be sufficient to close
+            # everything, so if we have a hold of the connection close that
+            # too.
+            if self._connection is not None:
+                self._connection.close()
+
             raise
         finally:
             if self._original_response and self._original_response.isclosed():


Test patch:
diff --git a/test/with_dummyserver/test_socketlevel.py b/test/with_dummyserver/test_socketlevel.py
--- a/test/with_dummyserver/test_socketlevel.py
+++ b/test/with_dummyserver/test_socketlevel.py
@@ -30,6 +30,7 @@
     class MimeToolMessage(object):
         pass
 from threading import Event
+import select
 import socket
 import ssl
 
@@ -366,6 +367,72 @@ def socket_handler(listener):
             self.assertRaises(ProtocolError, response.read)
             self.assertEqual(poolsize, pool.pool.qsize())
 
+    def test_connection_closed_on_read_timeout_preload_false(self):
+        timed_out = Event()
+
+        def socket_handler(listener):
+            sock = listener.accept()[0]
+
+            # Consume request
+            buf = b''
+            while not buf.endswith(b'\r\n\r\n'):
+                buf = sock.recv(65535)
+
+            # Send partial chunked response and then hang.
+            sock.send((
+                'HTTP/1.1 200 OK\r\n'
+                'Content-Type: text/plain\r\n'
+                'Transfer-Encoding: chunked\r\n'
+                '\r\n'
+                '8\r\n'
+                '12345678\r\n').encode('utf-8')
+            )
+            timed_out.wait(5)
+
+            # Expect a new request, but keep hold of the old socket to avoid
+            # leaking it. Because we don't want to hang this thread, we
+            # actually use select.select to confirm that a new request is
+            # coming in: this lets us time the thread out.
+            rlist, _, _ = select.select([listener], [], [], 1)
+            assert rlist
+            new_sock = listener.accept()[0]
+
+            # Consume request
+            buf = b''
+            while not buf.endswith(b'\r\n\r\n'):
+                buf = new_sock.recv(65535)
+
+            # Send complete chunked response.
+            new_sock.send((
+                'HTTP/1.1 200 OK\r\n'
+                'Content-Type: text/plain\r\n'
+                'Transfer-Encoding: chunked\r\n'
+                '\r\n'
+                '8\r\n'
+                '12345678\r\n'
+                '0\r\n\r\n').encode('utf-8')
+            )
+
+            new_sock.close()
+            sock.close()
+
+        self._start_server(socket_handler)
+        with HTTPConnectionPool(self.host, self.port) as pool:
+            # First request should fail.
+            response = pool.urlopen('GET', '/', retries=0,
+                                    preload_content=False,
+                                    timeout=Timeout(connect=1, read=0.001))
+            try:
+                self.assertRaises(ReadTimeoutError, response.read)
+            finally:
+                timed_out.set()
+
+            # Second should succeed.
+            response = pool.urlopen('GET', '/', retries=0,
+                                    preload_content=False,
+                                    timeout=Timeout(connect=1, read=0.1))
+            self.assertEqual(len(response.read()), 8)
+
 
 class TestProxyManager(SocketDummyServerTestCase):