Repository: getsentry/responses (Python)

Issue:
A stream request blocks on response.iter_content when chunk_size=None
A response is blocking on stream request while iterating over content with chunk_size=None

### Environment

responses==0.15.0 requests==2.26.0   on python 3.7.8, but I believe other python versions are affected as well.
The problem is new in version 0.15.0 and affects 0.16.0 too.

### Steps to Reproduce
Install requests and responses:
```
pip install responses==0.15.0 requests==2.26.0 
```
Run below code
```python
import responses
import requests

with responses.RequestsMock() as fakes:
    fakes.add(responses.GET, 'https://example.com', status=200,
            content_type='application/octet-stream',
            body=b'This is test',
            auto_calculate_content_length=True
            )
    res = requests.get('https://example.com', stream=True)
    for chunk in res.iter_content(chunk_size=None):
        print(chunk)
```

### Expected Result

A code prints out `b'This is test'` and returns. 

### Actual Result

A code prints out `b'This is test'` and blocks waiting for more data. As far as I could debug it's blocking on  [this loop of urllib3](https://github.com/urllib3/urllib3/blob/1.26.7/src/urllib3/response.py#L575-L579), which suggests that `responses` doesn't close a stream once it's fully read.



Discussion:
@NorbertWasilewski 
UDP: can reproduce on latest `master`
The problem is actually quite deep.
Right now `Response` objects uses `StringIO` to mock request body. Since object must be like a file.
but at the same time you call with `None`, and since we go by different path in `urllib3`, we face non-closed object. `urllib3` does not close object when called with `None` (see screenshot below)
I will perform further investigation to see if we can find workaround
![image](https://user-images.githubusercontent.com/51964909/142216675-7e1f7faf-c5f0-4721-b742-ced0b9183aff.png)




Patch:
diff --git a/CHANGES b/CHANGES
index 026429fb..2ec3a994 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,7 @@
 0.17.0
 ------
 
+* Fixed issue when `response.iter_content` when `chunk_size=None` entered infinite loop
 * Fixed issue when `passthru_prefixes` persisted across tests.
   Now `add_passthru` is valid only within a context manager or for a single function and
   cleared on exit
diff --git a/responses/__init__.py b/responses/__init__.py
index 8d5d3b82..afcd6576 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -247,7 +247,35 @@ def _handle_body(body):
     if isinstance(body, _io.BufferedReader):
         return body
 
-    return BufferIO(body)
+    data = BufferIO(body)
+
+    def is_closed():
+        """
+        Real Response uses HTTPResponse as body object.
+        Thus, when method is_closed is called first to check if there is any more
+        content to consume and the file-like object is still opened
+
+        This method ensures stability to work for both:
+        https://github.com/getsentry/responses/issues/438
+        https://github.com/getsentry/responses/issues/394
+
+        where file should be intentionally be left opened to continue consumption
+        """
+        if not data.closed and data.read(1):
+            # if there is more bytes to read then keep open, but return pointer
+            data.seek(-1, 1)
+            return False
+        else:
+            if not data.closed:
+                # close but return False to mock like is still opened
+                data.close()
+                return False
+
+            # only if file really closed (by us) return True
+            return True
+
+    data.isclosed = is_closed
+    return data
 
 
 class BaseResponse(object):


Test patch:
diff --git a/responses/test_responses.py b/responses/test_responses.py
index 748fb052..d48ce6b5 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -1219,6 +1219,9 @@ def test_content_length_error(monkeypatch):
     https://github.com/psf/requests/pull/3563
 
     Now user can manually patch URL3 lib to achieve the same
+
+    See discussion in
+    https://github.com/getsentry/responses/issues/394
     """
 
     @responses.activate
@@ -1248,6 +1251,30 @@ def patched_init(self, *args, **kwargs):
     assert_reset()
 
 
+def test_stream_with_none_chunk_size():
+    """
+    See discussion in
+    https://github.com/getsentry/responses/issues/438
+    """
+
+    @responses.activate
+    def run():
+        responses.add(
+            responses.GET,
+            "https://example.com",
+            status=200,
+            content_type="application/octet-stream",
+            body=b"This is test",
+            auto_calculate_content_length=True,
+        )
+        res = requests.get("https://example.com", stream=True)
+        for chunk in res.iter_content(chunk_size=None):
+            assert chunk == b"This is test"
+
+    run()
+    assert_reset()
+
+
 def test_legacy_adding_headers():
     @responses.activate
     def run():