Repository: urllib3/urllib3

Issue:
socket.timeout not wrapped properly
As mentioned in a [comment](https://github.com/kennethreitz/requests/issues/1787#issuecomment-30510686) on kennethreitz/requests#1787 urllib3 apparently lets a `socket.timeout` exception propagate outside library code instead of wrapping it e.g. with `urllib3.exceptions.TimeoutError`. See the linked issue for a full backtrace.



Discussion:
I've been going through their code. i wrote some code of my own to timeout:

``` python
import requests
print requests.__version__
r = requests.get('http://httpbin.org/delay/50', timeout=0.1)
```

```
2.1.0
Traceback (most recent call last):
  File "working_on_req.py", line 7, in <module>
    r = requests.get('http://httpbin.org/delay/50', timeout=0.1)
  File "/home/hackawaye/virtual_envs/requests_virtual/local/lib/python2.7/site-packages/requests-2.1.0-py2.7.egg/requests/api.py", line 55, in get
    return request('get', url, **kwargs)
  File "/home/hackawaye/virtual_envs/requests_virtual/local/lib/python2.7/site-packages/requests-2.1.0-py2.7.egg/requests/api.py", line 44, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/hackawaye/virtual_envs/requests_virtual/local/lib/python2.7/site-packages/requests-2.1.0-py2.7.egg/requests/sessions.py", line 382, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/hackawaye/virtual_envs/requests_virtual/local/lib/python2.7/site-packages/requests-2.1.0-py2.7.egg/requests/sessions.py", line 485, in send
    r = adapter.send(request, **kwargs)
  File "/home/hackawaye/virtual_envs/requests_virtual/local/lib/python2.7/site-packages/requests-2.1.0-py2.7.egg/requests/adapters.py", line 381, in send
    raise Timeout(e)
requests.exceptions.Timeout: (<requests.packages.urllib3.connectionpool.HTTPConnectionPool object at 0x13d6490>, 'Connection to httpbin.org timed out. (connect timeout=0.1)')
```

This is all i get. I noticed you also have an auth parameter but i fail to see how it changes the issue.

Can you provide me with the url you were trying to get when you ran into this?

It was a local connection; there was nothing running on the port when it happened (so it shouldn't have timed out in the first first place but simply ended up in a failed connection)

Ok. So i found where the problem is, it should not occur, it is a problem with the socket.py in that the stream is not properly ended and the socket.recv gets confused and halts giving you the timeout.
I can wrap it but it will not be a proper fix for this.
I tried to submit a fix for it a while back but i can't remember exactly what char i encountered that broke it this way.
It might be caused by the server as well.

@ThiefMaster  does this occur when using urllib3 installed form source and requests installed from source?


Resolving patch:
diff --git a/urllib3/response.py b/urllib3/response.py
--- a/urllib3/response.py
+++ b/urllib3/response.py
@@ -5,19 +5,16 @@
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
 
 
-import logging
 import zlib
 import io
+from socket import timeout as SocketTimeout
 
 from ._collections import HTTPHeaderDict
-from .exceptions import DecodeError
+from .exceptions import DecodeError, ReadTimeoutError
 from .packages.six import string_types as basestring, binary_type
 from .util import is_fp_closed
 
 
-log = logging.getLogger(__name__)
-
-
 class DeflateDecoder(object):
 
     def __init__(self):
@@ -178,23 +175,29 @@ def read(self, amt=None, decode_content=None, cache_content=False):
         flush_decoder = False
 
         try:
-            if amt is None:
-                # cStringIO doesn't like amt=None
-                data = self._fp.read()
-                flush_decoder = True
-            else:
-                cache_content = False
-                data = self._fp.read(amt)
-                if amt != 0 and not data:  # Platform-specific: Buggy versions of Python.
-                    # Close the connection when no data is returned
-                    #
-                    # This is redundant to what httplib/http.client _should_
-                    # already do.  However, versions of python released before
-                    # December 15, 2012 (http://bugs.python.org/issue16298) do
-                    # not properly close the connection in all cases. There is
-                    # no harm in redundantly calling close.
-                    self._fp.close()
+            try:
+                if amt is None:
+                    # cStringIO doesn't like amt=None
+                    data = self._fp.read()
                     flush_decoder = True
+                else:
+                    cache_content = False
+                    data = self._fp.read(amt)
+                    if amt != 0 and not data:  # Platform-specific: Buggy versions of Python.
+                        # Close the connection when no data is returned
+                        #
+                        # This is redundant to what httplib/http.client _should_
+                        # already do.  However, versions of python released before
+                        # December 15, 2012 (http://bugs.python.org/issue16298) do
+                        # not properly close the connection in all cases. There is
+                        # no harm in redundantly calling close.
+                        self._fp.close()
+                        flush_decoder = True
+
+            except SocketTimeout:
+                # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but
+                # there is yet no clean way to get at it from this context.
+                raise ReadTimeoutError(self._pool, None, 'Read timed out.')
 
             self._fp_bytes_read += len(data)
 
@@ -204,8 +207,7 @@ def read(self, amt=None, decode_content=None, cache_content=False):
             except (IOError, zlib.error) as e:
                 raise DecodeError(
                     "Received response with content-encoding: %s, but "
-                    "failed to decode it." % content_encoding,
-                    e)
+                    "failed to decode it." % content_encoding, e)
 
             if flush_decoder and decode_content and self._decoder:
                 buf = self._decoder.decompress(binary_type())
@@ -296,7 +298,7 @@ def fileno(self):
         elif hasattr(self._fp, "fileno"):
             return self._fp.fileno()
         else:
-            raise IOError("The file-like object  this HTTPResponse is wrapped "
+            raise IOError("The file-like object this HTTPResponse is wrapped "
                           "around has no file descriptor")
 
     def flush(self):


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
@@ -173,6 +173,32 @@ def socket_handler(listener):
         finally:
             socket.setdefaulttimeout(default_timeout)
 
+    def test_delayed_body_read_timeout(self):
+        timed_out = Event()
+
+        def socket_handler(listener):
+            sock = listener.accept()[0]
+            buf = b''
+            body = 'Hi'
+            while not buf.endswith(b'\r\n\r\n'):
+                buf = sock.recv(65536)
+            sock.send(('HTTP/1.1 200 OK\r\n'
+                       'Content-Type: text/plain\r\n'
+                       'Content-Length: %d\r\n'
+                       '\r\n' % len(body)).encode('utf-8'))
+
+            timed_out.wait(timeout=0.5)
+            sock.send(body.encode('utf-8'))
+            sock.close()
+
+        self._start_server(socket_handler)
+        pool = HTTPConnectionPool(self.host, self.port)
+
+        response = pool.urlopen('GET', '/', retries=0, preload_content=False,
+                                timeout=util.Timeout(connect=1, read=0.001))
+        self.assertRaises(ReadTimeoutError, response.read)
+        timed_out.set()
+
 
 class TestProxyManager(SocketDummyServerTestCase):