Repository: getsentry/responses (Python)

Issue:
AttributeError when urllib3 throws an IncompleteRead
**TL;DR:** There is a flag in `urllib3.HTTPResponse` that checks if the "Content-Length" header of a HTTP request matches its body size. If this flag is set, the error raised by a mismatch between the two causes an AttributeError in responses (see further below for details).
With the current version of requests, the required flag in urllib3 has to be patched in but it is also planned to be included in the requests library with their planned release of 3.0.0 (https://github.com/psf/requests/issues/4956#issue-405545488).

### Environment

python 3.8.5
requests==2.26.0
responses==0.13.3
urllib3==1.26.6


### Steps to Reproduce

Minimal working example:
```python
import unittest

import requests
import responses
import urllib3
from requests.exceptions import ChunkedEncodingError


def patch_urllib3():
    """Set urllib3's enforce_content_length to True by default."""
    previous_init = urllib3.HTTPResponse.__init__
    def new_init(self, *args, **kwargs):
        previous_init(self, *args, enforce_content_length = True, **kwargs)
    urllib3.HTTPResponse.__init__ = new_init

class TestResponses(unittest.TestCase):
    """Test that responses handles IncompleteRead exceptions correctly."""

    @responses.activate
    def test_raises_an_error(self):
        """Test that an IncorrectEncoding is thrown."""
        patch_urllib3()
        responses.add(**{
            "method"            : responses.GET,
            "url"               : "http://example.com/api/123",
            "body"              : "{'message': 'this body is too large'}",
            "adding_headers"    : {"content-length": "2"}
        })
        with self.assertRaises(ChunkedEncodingError):
            requests.get("http://example.com/api/123")
```

### Expected Result

When building a working example with fastapi and uvicorn, a `requests.exceptions.ChunkedEncodingError` is thrown. I would expect this to also happen when responses is used. If a simple
```python
def close(self):
    return
```
is added to `responses/__init__.py:OriginalResponseShim`, this is also the result that occurs with responses. 

### Actual Result

The actual result is an `AttributeError`:

```
Traceback (most recent call last):
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/urllib3/response.py", line 438, in _error_catcher
    yield
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/urllib3/response.py", line 541, in read
    raise IncompleteRead(self._fp_bytes_read, self.length_remaining)
urllib3.exceptions.IncompleteRead: IncompleteRead(37 bytes read, -35 more expected)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/urllib3/response.py", line 455, in _error_catcher
    raise ProtocolError("Connection broken: %r" % e, e)
urllib3.exceptions.ProtocolError: ('Connection broken: IncompleteRead(37 bytes read, -35 more expected)', IncompleteRead(37 bytes read, -35 more expected))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<string>", line 3, in wrapper
  File "/local/home/fma/dev/pacs-interface/tests/test_minimal_exaple.py", line 32, in test_raises_an_error
    requests.get("http://example.com/api/123")
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/requests/api.py", line 75, in get
    return request('get', url, params=params, **kwargs)
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/requests/api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/requests/sessions.py", line 542, in request
    resp = self.send(prep, **send_kwargs)
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/requests/sessions.py", line 655, in send
    r = adapter.send(request, **kwargs)
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/responses/__init__.py", line 768, in unbound_on_send
    return self._on_request(adapter, request, *a, **kwargs)
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/responses/__init__.py", line 759, in _on_request
    response.content  # NOQA
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/requests/models.py", line 836, in content
    self._content = b''.join(self.iter_content(CONTENT_CHUNK_SIZE)) or b''
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/requests/models.py", line 758, in generate
    for chunk in self.raw.stream(chunk_size, decode_content=True):
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/urllib3/response.py", line 576, in stream
    data = self.read(amt=amt, decode_content=decode_content)
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/urllib3/response.py", line 541, in read
    raise IncompleteRead(self._fp_bytes_read, self.length_remaining)
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/contextlib.py", line 131, in __exit__
    self.gen.throw(type, value, traceback)
  File "/local/home/fma/.conda/envs/endpoint/lib/python3.8/site-packages/urllib3/response.py", line 468, in _error_catcher
    self._original_response.close()
AttributeError: 'OriginalResponseShim' object has no attribute 'close'
```



Discussion:

Patch:
diff --git a/responses/__init__.py b/responses/__init__.py
index 7de3b644..5000ecd3 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -515,6 +515,9 @@ def __init__(self, headers):
     def isclosed(self):
         return True
 
+    def close(self):
+        return
+
 
 class RequestsMock(object):
     DELETE = "DELETE"


Test patch:
diff --git a/responses/test_responses.py b/responses/test_responses.py
index 9c02fe6e..8e6f68de 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -12,7 +12,7 @@
 import pytest
 import requests
 import responses
-from requests.exceptions import ConnectionError, HTTPError
+from requests.exceptions import ConnectionError, HTTPError, ChunkedEncodingError
 from responses import (
     BaseResponse,
     Response,
@@ -1177,6 +1177,43 @@ def run():
     assert_reset()
 
 
+def test_content_length_error(monkeypatch):
+    """
+    Currently 'requests' does not enforce content length validation,
+    (validation that body length matches header). However, this could
+    be expected in next major version, see
+    https://github.com/psf/requests/pull/3563
+
+    Now user can manually patch URL3 lib to achieve the same
+    """
+
+    @responses.activate
+    def run():
+        responses.add(
+            responses.GET,
+            "http://example.com/api/123",
+            json={"message": "this body is too large"},
+            adding_headers={"content-length": "2"},
+        )
+        with pytest.raises(ChunkedEncodingError) as exc:
+            requests.get("http://example.com/api/123")
+
+        assert "IncompleteRead" in str(exc.value)
+
+    original_init = getattr(requests.packages.urllib3.HTTPResponse, "__init__")
+
+    def patched_init(self, *args, **kwargs):
+        kwargs["enforce_content_length"] = True
+        original_init(self, *args, **kwargs)
+
+    monkeypatch.setattr(
+        requests.packages.urllib3.HTTPResponse, "__init__", patched_init
+    )
+
+    run()
+    assert_reset()
+
+
 def test_legacy_adding_headers():
     @responses.activate
     def run():