Repository: urllib3/urllib3

Issue:
If the response is complete, stream/read_chunked should short-circuit return the empty string.
Originally seen in kennethreitz/requests#3807.

Basically, read_chunked should check whether `_fp` is `None`. If it is, it should immediately return the empty byte string rather than do any processing.

Put another way, this shouldn't blow up:

```python
import urllib3

http = urllib3.PoolManager()
resp = http.request('GET', 'http://http2bin.org/stream/100')
resp.read()
next(resp.read_chunked())
```

I recommend someone who wants to help out pick this issue up, it should be easily fixed and tested.


Discussion:
I believe we already check that `_fp` isn't None [here](https://github.com/shazow/urllib3/blob/master/urllib3/response.py#L518-L525), right? I think the actual issue is we're not checking that `_fp.fp` isn't None which signals the connection has been closed (and hopefully read).
Can we not just ask the fp directly whether it's closed?
@Lukasa, yeah, it looks like `closed` on the Response object will correctly check this for us.
I was about to go and fix this -- shouldn't we just `return` so that `StopIteration` is raised instead of returning `b''`?
Any updates on this? I'm getting a ton of these in a recent project. Thank you! 

Resolving patch:
diff --git a/urllib3/response.py b/urllib3/response.py
--- a/urllib3/response.py
+++ b/urllib3/response.py
@@ -600,6 +600,11 @@ def read_chunked(self, amt=None, decode_content=None):
         Similar to :meth:`HTTPResponse.read`, but with an additional
         parameter: ``decode_content``.
 
+        :param amt:
+            How much of the content to read. If specified, caching is skipped
+            because it doesn't make sense to cache partial content as the full
+            response.
+
         :param decode_content:
             If True, will attempt to decode the body based on the
             'content-encoding' header.
@@ -621,6 +626,11 @@ def read_chunked(self, amt=None, decode_content=None):
                 self._original_response.close()
                 return
 
+            # If a response is already read and closed
+            # then return immediately.
+            if self._fp.fp is None:
+                return
+
             while True:
                 self._update_chunk_length()
                 if self.chunk_left == 0:


Test patch:
diff --git a/test/appengine/test_gae_manager.py b/test/appengine/test_gae_manager.py
--- a/test/appengine/test_gae_manager.py
+++ b/test/appengine/test_gae_manager.py
@@ -105,6 +105,8 @@ def test_exceptions(self):
     test_release_conn_parameter = None
     test_stream_keepalive = None
     test_cleanup_on_connection_error = None
+    test_read_chunked_short_circuit = None
+    test_read_chunked_on_closed_response = None
 
     # Tests that should likely be modified for appengine specific stuff
     test_timeout = None
diff --git a/test/with_dummyserver/test_connectionpool.py b/test/with_dummyserver/test_connectionpool.py
--- a/test/with_dummyserver/test_connectionpool.py
+++ b/test/with_dummyserver/test_connectionpool.py
@@ -5,6 +5,7 @@
 import unittest
 import time
 import warnings
+import pytest
 
 import mock
 
@@ -695,6 +696,26 @@ def test_stream_keepalive(self):
         self.assertEqual(self.pool.num_connections, 1)
         self.assertEqual(self.pool.num_requests, x)
 
+    def test_read_chunked_short_circuit(self):
+        response = self.pool.request(
+            'GET',
+            '/chunked',
+            preload_content=False
+        )
+        response.read()
+        with pytest.raises(StopIteration):
+            next(response.read_chunked())
+
+    def test_read_chunked_on_closed_response(self):
+        response = self.pool.request(
+            'GET',
+            '/chunked',
+            preload_content=False
+        )
+        response.close()
+        with pytest.raises(StopIteration):
+            next(response.read_chunked())
+
     def test_chunked_gzip(self):
         response = self.pool.request(
                 'GET',