Repository: getsentry/responses

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:
codecov[bot]: # [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/433?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
> Merging [#433](https://app.codecov.io/gh/getsentry/responses/compare/433?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (81cb5d7) into [master](https://codecov.io/gh/getsentry/responses/commit/3e029949c8f3c1b3b2b48f9e437fedd7cf27746d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (3e02994) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/getsentry/responses/pull/433/graphs/tree.svg?width=650&height=150&src=pr&token=x2tAGR7rxE&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)](https://app.codecov.io/gh/getsentry/responses/compare/433?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)

```diff
@@            Coverage Diff            @@
##            master      #433   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            3         3           
  Lines         1875      1892   +17     
=========================================
+ Hits          1875      1892   +17     
```


| [Impacted Files](https://app.codecov.io/gh/getsentry/responses/compare/433?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) | Coverage Δ | |
|---|---|---|
| [responses/\_\_init\_\_.py](https://app.codecov.io/gh/getsentry/responses/compare/433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [responses/test\_responses.py](https://app.codecov.io/gh/getsentry/responses/compare/433/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RfcmVzcG9uc2VzLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/433?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/433?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [3e02994...81cb5d7](https://app.codecov.io/gh/getsentry/responses/compare/433?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry).


Patch:
diff --git a/CHANGES b/CHANGES
index 60a0617..3505ba9 100644
--- a/CHANGES
+++ b/CHANGES
@@ -3,11 +3,11 @@
 
 * Fixed regression with `stream` parameter deprecation, requests.session() and cookie handling.
 * Replaced adhoc URL parsing with `urllib.parse`.
+* Added ``match`` parameter to ``add_callback`` method
 * Added `responses.matchers.fragment_identifier_matcher`. This matcher allows you
   to match request URL fragment identifier.
 * Improved test coverage.
 
-
 0.15.0
 ------
 
diff --git a/README.rst b/README.rst
index f04dfb3..7f7857f 100644
--- a/README.rst
+++ b/README.rst
@@ -7,6 +7,9 @@ Responses
 .. image:: https://img.shields.io/pypi/pyversions/responses.svg
     :target: https://pypi.org/project/responses/
 
+.. image:: https://codecov.io/gh/getsentry/responses/branch/master/graph/badge.svg
+    :target: https://codecov.io/gh/getsentry/responses/
+
 A utility library for mocking out the ``requests`` Python library.
 
 ..  note::
diff --git a/mypy.ini b/mypy.ini
index 8e109ff..ee614e0 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -35,3 +35,8 @@ ignore_missing_imports=True
 disallow_untyped_defs=False
 disallow_untyped_calls=False
 disallow_any_decorated=False
+
+[mypy-responses.test_matchers]
+disallow_untyped_defs=False
+disallow_untyped_calls=False
+disallow_any_decorated=False
diff --git a/responses/__init__.py b/responses/__init__.py
index 7de3b64..925d13e 100644
--- a/responses/__init__.py
+++ b/responses/__init__.py
@@ -259,7 +259,7 @@ class BaseResponse(object):
 
     stream = False
 
-    def __init__(self, method, url, match_querystring=_unspecified, match=[]):
+    def __init__(self, method, url, match_querystring=_unspecified, match=()):
         self.method = method
         # ensure the url has a default path set if the url is a string
         self.url = _ensure_url_default_path(url)
@@ -515,6 +515,9 @@ class OriginalResponseShim(object):
     def isclosed(self):
         return True
 
+    def close(self):
+        return
+
 
 class RequestsMock(object):
     DELETE = "DELETE"
@@ -667,7 +670,13 @@ class RequestsMock(object):
             self.add(method_or_response, url, body, *args, **kwargs)
 
     def add_callback(
-        self, method, url, callback, match_querystring=False, content_type="text/plain"
+        self,
+        method,
+        url,
+        callback,
+        match_querystring=False,
+        content_type="text/plain",
+        match=(),
     ):
         # ensure the url has a default path set if the url is a string
         # url = _ensure_url_default_path(url, match_querystring)
@@ -679,6 +688,7 @@ class RequestsMock(object):
                 callback=callback,
                 content_type=content_type,
                 match_querystring=match_querystring,
+                match=match,
             )
         )
 
diff --git a/responses/__init__.pyi b/responses/__init__.pyi
index 8e0bf35..898c68a 100644
--- a/responses/__init__.pyi
+++ b/responses/__init__.pyi
@@ -11,7 +11,8 @@ from typing import (
     Dict,
     List,
     Tuple,
-    Union
+    Union,
+    Iterable
 )
 from io import BufferedReader, BytesIO
 from re import Pattern
@@ -47,6 +48,8 @@ class Call(NamedTuple):
 
 _Body = Union[str, BaseException, "Response", BufferedReader, bytes]
 
+MatcherIterable = Iterable[Callable[[Any], Callable[..., Any]]]
+
 class CallList(Sequence[Call], Sized):
     def __init__(self) -> None: ...
     def __iter__(self) -> Iterator[Call]: ...
@@ -63,19 +66,19 @@ class BaseResponse:
     method: Any = ...
     url: Any = ...
     match_querystring: Any = ...
-    match: List[Any] = ...
+    match: MatcherIterable = ...
     call_count: int = ...
     def __init__(
         self,
         method: str,
         url: Union[Pattern[str], str],
         match_querystring: Union[bool, object] = ...,
-        match: List[Callable[..., Any]] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
     def __eq__(self, other: Any) -> bool: ...
     def __ne__(self, other: Any) -> bool: ...
     def _req_attr_matches(
-        self, match: List[Callable[..., Any]], request: Optional[Union[bytes, str]]
+        self, match: MatcherIterable, request: Optional[Union[bytes, str]]
     ) -> Tuple[bool, str]: ...
     def _should_match_querystring(
         self, match_querystring_argument: Union[bool, object]
@@ -107,7 +110,7 @@ class Response(BaseResponse):
         content_type: Optional[str] = ...,
         auto_calculate_content_length: bool = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
     def get_response(  # type: ignore [override]
         self, request: PreparedRequest
@@ -125,7 +128,7 @@ class CallbackResponse(BaseResponse):
         stream: bool = ...,
         content_type: Optional[str] = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
     def get_response(  # type: ignore [override]
         self, request: PreparedRequest
@@ -202,7 +205,7 @@ class _Add(Protocol):
         auto_calculate_content_length: bool = ...,
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
 
 class _AddCallback(Protocol):
@@ -213,6 +216,7 @@ class _AddCallback(Protocol):
         callback: Callable[[PreparedRequest], Union[Exception, Tuple[int, Mapping[str, str], _Body]]],
         match_querystring: bool = ...,
         content_type: Optional[str] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
 
 class _AddPassthru(Protocol):
@@ -240,7 +244,7 @@ class _Replace(Protocol):
         content_type: Optional[str] = ...,
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
 
 class _Upsert(Protocol):
@@ -256,7 +260,7 @@ class _Upsert(Protocol):
         content_type: Optional[str] = ...,
         adding_headers: HeaderSet = ...,
         match_querystring: bool = ...,
-        match: List[Any] = ...,
+        match: MatcherIterable = ...,
     ) -> None: ...
 
 class _Registered(Protocol):


Test patch:
diff --git a/responses/test_matchers.py b/responses/test_matchers.py
new file mode 100644
index 0000000..ba6afb3
--- /dev/null
+++ b/responses/test_matchers.py
@@ -0,0 +1,614 @@
+from __future__ import absolute_import, print_function, division, unicode_literals
+
+import six
+from sys import version_info
+
+import pytest
+import requests
+import responses
+from requests.exceptions import ConnectionError
+from responses import matchers
+
+
+def assert_response(resp, body=None, content_type="text/plain"):
+    assert resp.status_code == 200
+    assert resp.reason == "OK"
+    assert resp.headers["Content-Type"] == content_type
+    assert resp.text == body
+
+
+def assert_reset():
+    assert len(responses._default_mock._matches) == 0
+    assert len(responses.calls) == 0
+
+
+def test_query_string_matcher():
+    @responses.activate
+    def run():
+        url = "http://example.com?test=1&foo=bar"
+        responses.add(
+            responses.GET,
+            url,
+            body=b"test",
+            match=[matchers.query_string_matcher("test=1&foo=bar")],
+        )
+        resp = requests.get("http://example.com?test=1&foo=bar")
+        assert_response(resp, "test")
+        resp = requests.get("http://example.com?foo=bar&test=1")
+        assert_response(resp, "test")
+        resp = requests.get("http://example.com/?foo=bar&test=1")
+        assert_response(resp, "test")
+
+    run()
+    assert_reset()
+
+
+def test_request_matches_post_params():
+    @responses.activate
+    def run(deprecated):
+        if deprecated:
+            json_params_matcher = getattr(responses, "json_params_matcher")
+            urlencoded_params_matcher = getattr(responses, "urlencoded_params_matcher")
+        else:
+            json_params_matcher = matchers.json_params_matcher
+            urlencoded_params_matcher = matchers.urlencoded_params_matcher
+
+        responses.add(
+            method=responses.POST,
+            url="http://example.com/",
+            body="one",
+            match=[json_params_matcher({"page": {"name": "first", "type": "json"}})],
+        )
+        responses.add(
+            method=responses.POST,
+            url="http://example.com/",
+            body="two",
+            match=[urlencoded_params_matcher({"page": "second", "type": "urlencoded"})],
+        )
+
+        resp = requests.request(
+            "POST",
+            "http://example.com/",
+            headers={"Content-Type": "x-www-form-urlencoded"},
+            data={"page": "second", "type": "urlencoded"},
+        )
+        assert_response(resp, "two")
+
+        resp = requests.request(
+            "POST",
+            "http://example.com/",
+            headers={"Content-Type": "application/json"},
+            json={"page": {"name": "first", "type": "json"}},
+        )
+        assert_response(resp, "one")
+
+    with pytest.deprecated_call():
+        run(deprecated=True)
+        assert_reset()
+
+    run(deprecated=False)
+    assert_reset()
+
+
+def test_request_matches_empty_body():
+    def run():
+        with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps:
+            # test that both json and urlencoded body are empty in matcher and in request
+            rsps.add(
+                method=responses.POST,
+                url="http://example.com/",
+                body="one",
+                match=[matchers.json_params_matcher(None)],
+            )
+
+            rsps.add(
+                method=responses.POST,
+                url="http://example.com/",
+                body="two",
+                match=[matchers.urlencoded_params_matcher(None)],
+            )
+
+            resp = requests.request("POST", "http://example.com/")
+            assert_response(resp, "one")
+
+            resp = requests.request(
+                "POST",
+                "http://example.com/",
+                headers={"Content-Type": "x-www-form-urlencoded"},
+            )
+            assert_response(resp, "two")
+
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            # test exception raise if matcher body is None but request data is not None
+            rsps.add(
+                method=responses.POST,
+                url="http://example.com/",
+                body="one",
+                match=[matchers.json_params_matcher(None)],
+            )
+
+            with pytest.raises(ConnectionError) as excinfo:
+                resp = requests.request(
+                    "POST",
+                    "http://example.com/",
+                    json={"my": "data"},
+                    headers={"Content-Type": "application/json"},
+                )
+
+            msg = str(excinfo.value)
+            assert "request.body doesn't match: {my: data} doesn't match {}" in msg
+
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            rsps.add(
+                method=responses.POST,
+                url="http://example.com/",
+                body="two",
+                match=[matchers.urlencoded_params_matcher(None)],
+            )
+            with pytest.raises(ConnectionError) as excinfo:
+                resp = requests.request(
+                    "POST",
+                    "http://example.com/",
+                    headers={"Content-Type": "x-www-form-urlencoded"},
+                    data={"page": "second", "type": "urlencoded"},
+                )
+            msg = str(excinfo.value)
+            assert (
+                "request.body doesn't match: {page: second, type: urlencoded} doesn't match {}"
+                in msg
+            )
+
+    run()
+    assert_reset()
+
+
+def test_request_matches_params():
+    @responses.activate
+    def run():
+        url = "http://example.com/test"
+        params = {"hello": "world", "I am": "a big test"}
+        responses.add(
+            method=responses.GET,
+            url=url,
+            body="test",
+            match=[matchers.query_param_matcher(params)],
+            match_querystring=False,
+        )
+
+        # exchange parameter places for the test
+        params = {
+            "I am": "a big test",
+            "hello": "world",
+        }
+        resp = requests.get(url, params=params)
+
+        if six.PY3 and version_info[1] >= 7:
+            # only after py 3.7 dictionaries are ordered, so we can check URL
+            constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world"
+            assert resp.url == constructed_url
+            assert resp.request.url == constructed_url
+
+        resp_params = getattr(resp.request, "params")
+        assert resp_params == params
+
+    run()
+    assert_reset()
+
+
+def test_fail_matchers_error():
+    """
+    Validate that Exception is raised if request does not match responses.matchers
+        validate matchers.urlencoded_params_matcher
+        validate matchers.json_params_matcher
+        validate matchers.query_param_matcher
+        validate matchers.request_kwargs_matcher
+    :return: None
+    """
+
+    def run():
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            rsps.add(
+                "POST",
+                "http://example.com",
+                match=[matchers.urlencoded_params_matcher({"foo": "bar"})],
+            )
+            rsps.add(
+                "POST",
+                "http://example.com",
+                match=[matchers.json_params_matcher({"fail": "json"})],
+            )
+
+            with pytest.raises(ConnectionError) as excinfo:
+                requests.post("http://example.com", data={"id": "bad"})
+
+            msg = str(excinfo.value)
+            assert (
+                "request.body doesn't match: {id: bad} doesn't match {foo: bar}" in msg
+            )
+
+            assert (
+                "request.body doesn't match: JSONDecodeError: Cannot parse request.body"
+                in msg
+            )
+
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            rsps.add(
+                "GET",
+                "http://111.com",
+                match=[matchers.query_param_matcher({"my": "params"})],
+            )
+
+            rsps.add(
+                method=responses.GET,
+                url="http://111.com/",
+                body="two",
+                match=[matchers.json_params_matcher({"page": "one"})],
+            )
+
+            with pytest.raises(ConnectionError) as excinfo:
+                requests.get(
+                    "http://111.com", params={"id": "bad"}, json={"page": "two"}
+                )
+
+            msg = str(excinfo.value)
+            assert (
+                "Parameters do not match. {id: bad} doesn't match {my: params}" in msg
+            )
+            assert (
+                "request.body doesn't match: {page: two} doesn't match {page: one}"
+                in msg
+            )
+
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            req_kwargs = {
+                "stream": True,
+                "verify": False,
+            }
+            rsps.add(
+                "GET",
+                "http://111.com",
+                match=[matchers.request_kwargs_matcher(req_kwargs)],
+            )
+
+            with pytest.raises(ConnectionError) as excinfo:
+                requests.get("http://111.com", stream=True)
+
+            msg = str(excinfo.value)
+            assert (
+                "Arguments don't match: "
+                "{stream: True, verify: True} doesn't match {stream: True, verify: False}"
+            ) in msg
+
+    run()
+    assert_reset()
+
+
+def test_multipart_matcher():
+    @responses.activate
+    def run():
+        req_data = {"some": "other", "data": "fields"}
+        req_files = {"file_name": b"Old World!"}
+        responses.add(
+            responses.POST,
+            url="http://httpbin.org/post",
+            match=[matchers.multipart_matcher(req_files, data=req_data)],
+        )
+        resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files)
+        assert resp.status_code == 200
+
+        with pytest.raises(TypeError):
+            responses.add(
+                responses.POST,
+                url="http://httpbin.org/post",
+                match=[matchers.multipart_matcher(files={})],
+            )
+
+    run()
+    assert_reset()
+
+
+def test_multipart_matcher_fail():
+    """
+    Validate that Exception is raised if request does not match responses.matchers
+        validate matchers.multipart_matcher
+    :return: None
+    """
+
+    def run():
+        # different file contents
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            req_data = {"some": "other", "data": "fields"}
+            req_files = {"file_name": b"Old World!"}
+            rsps.add(
+                responses.POST,
+                url="http://httpbin.org/post",
+                match=[matchers.multipart_matcher(req_files, data=req_data)],
+            )
+
+            with pytest.raises(ConnectionError) as excinfo:
+                requests.post(
+                    "http://httpbin.org/post",
+                    data=req_data,
+                    files={"file_name": b"New World!"},
+                )
+
+            msg = str(excinfo.value)
+            assert "multipart/form-data doesn't match. Request body differs." in msg
+            assert (
+                '\r\nContent-Disposition: form-data; name="file_name"; '
+                'filename="file_name"\r\n\r\nOld World!\r\n'
+            ) in msg
+            assert (
+                '\r\nContent-Disposition: form-data; name="file_name"; '
+                'filename="file_name"\r\n\r\nNew World!\r\n'
+            ) in msg
+
+        # x-www-form-urlencoded request
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            req_data = {"some": "other", "data": "fields"}
+            req_files = {"file_name": b"Old World!"}
+            rsps.add(
+                responses.POST,
+                url="http://httpbin.org/post",
+                match=[matchers.multipart_matcher(req_files, data=req_data)],
+            )
+
+            with pytest.raises(ConnectionError) as excinfo:
+                requests.post("http://httpbin.org/post", data=req_data)
+
+            msg = str(excinfo.value)
+            assert (
+                "multipart/form-data doesn't match. Request headers['Content-Type'] is different."
+                in msg
+            )
+            assert (
+                "application/x-www-form-urlencoded isn't equal to multipart/form-data; boundary="
+                in msg
+            )
+
+        # empty body request
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            req_files = {"file_name": b"Old World!"}
+            rsps.add(
+                responses.POST,
+                url="http://httpbin.org/post",
+                match=[matchers.multipart_matcher(req_files)],
+            )
+
+            with pytest.raises(ConnectionError) as excinfo:
+                requests.post("http://httpbin.org/post")
+
+            msg = str(excinfo.value)
+            assert "Request is missing the 'Content-Type' header" in msg
+
+    run()
+    assert_reset()
+
+
+def test_query_string_matcher_raises():
+    """
+    Validate that Exception is raised if request does not match responses.matchers
+        validate matchers.query_string_matcher
+            :return: None
+    """
+
+    def run():
+        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
+            rsps.add(
+                "GET",
+                "http://111.com",
+                match=[matchers.query_string_matcher("didi=pro")],
+            )
+
+            with pytest.raises(ConnectionError) as excinfo:
+                requests.get("http://111.com", params={"test": "1", "didi": "pro"})
+
+            msg = str(excinfo.value)
+            assert (
+                "Query string doesn't match. {didi: pro, test: 1} doesn't match {didi: pro}"
+                in msg
+            )
+
+    run()
+    assert_reset()
+
+
+def test_request_matches_headers():
+    @responses.activate
+    def run():
+        url = "http://example.com/"
+        responses.add(
+            method=responses.GET,
+            url=url,
+            json={"success": True},
+            match=[matchers.header_matcher({"Accept": "application/json"})],
+        )
+
+        responses.add(
+            method=responses.GET,
+            url=url,
+            body="success",
+            match=[matchers.header_matcher({"Accept": "text/plain"})],
+        )
+
+        # the actual request can contain extra headers (requests always adds some itself anyway)
+        resp = requests.get(
+            url, headers={"Accept": "application/json", "Accept-Charset": "utf-8"}
+        )
+        assert_response(resp, body='{"success": true}', content_type="application/json")
+
+        resp = requests.get(url, headers={"Accept": "text/plain"})
+        assert_response(resp, body="success", content_type="text/plain")
+
+    run()
+    assert_reset()
+
+
+def test_request_matches_headers_no_match():
+    @responses.activate
+    def run():
+        url = "http://example.com/"
+        responses.add(
+            method=responses.GET,
+            url=url,
+            json={"success": True},
+            match=[matchers.header_matcher({"Accept": "application/json"})],
+        )
+
+        with pytest.raises(ConnectionError) as excinfo:
+            requests.get(url, headers={"Accept": "application/xml"})
+
+        msg = str(excinfo.value)
+        assert (
+            "Headers do not match: {Accept: application/xml} doesn't match "
+            "{Accept: application/json}"
+        ) in msg
+
+    run()
+    assert_reset()
+
+
+def test_request_matches_headers_strict_match():
+    @responses.activate
+    def run():
+        url = "http://example.com/"
+        responses.add(
+            method=responses.GET,
+            url=url,
+            body="success",
+            match=[
+                matchers.header_matcher({"Accept": "text/plain"}, strict_match=True)
+            ],
+        )
+
+        # requests will add some extra headers of its own, so we have to use prepared requests
+        session = requests.Session()
+
+        # make sure we send *just* the header we're expectin
+        prepped = session.prepare_request(
+            requests.Request(
+                method="GET",
+                url=url,
+            )
+        )
+        prepped.headers.clear()
+        prepped.headers["Accept"] = "text/plain"
+
+        resp = session.send(prepped)
+        assert_response(resp, body="success", content_type="text/plain")
+
+        # include the "Accept-Charset" header, which will fail to match
+        prepped = session.prepare_request(
+            requests.Request(
+                method="GET",
+                url=url,
+            )
+        )
+        prepped.headers.clear()
+        prepped.headers["Accept"] = "text/plain"
+        prepped.headers["Accept-Charset"] = "utf-8"
+
+        with pytest.raises(ConnectionError) as excinfo:
+            session.send(prepped)
+
+        msg = str(excinfo.value)
+        assert (
+            "Headers do not match: {Accept: text/plain, Accept-Charset: utf-8} "
+            "doesn't match {Accept: text/plain}"
+        ) in msg
+
+    run()
+    assert_reset()
+
+
+def test_fragment_identifier_matcher():
+    @responses.activate
+    def run():
+        responses.add(
+            responses.GET,
+            "http://example.com",
+            match=[matchers.fragment_identifier_matcher("test=1&foo=bar")],
+            body=b"test",
+        )
+
+        resp = requests.get("http://example.com#test=1&foo=bar")
+        assert_response(resp, "test")
+
+    run()
+    assert_reset()
+
+
+def test_fragment_identifier_matcher_error():
+    @responses.activate
+    def run():
+        responses.add(
+            responses.GET,
+            "http://example.com/",
+            match=[matchers.fragment_identifier_matcher("test=1")],
+        )
+        responses.add(
+            responses.GET,
+            "http://example.com/",
+            match=[matchers.fragment_identifier_matcher(None)],
+        )
+
+        with pytest.raises(ConnectionError) as excinfo:
+            requests.get("http://example.com/#test=2")
+
+        msg = str(excinfo.value)
+        assert (
+            "URL fragment identifier is different: test=1 doesn't match test=2"
+        ) in msg
+        assert (
+            "URL fragment identifier is different: None doesn't match test=2"
+        ) in msg
+
+    run()
+    assert_reset()
+
+
+def test_fragment_identifier_matcher_and_match_querystring():
+    @responses.activate
+    def run():
+        url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
+        responses.add(
+            responses.GET,
+            url,
+            match_querystring=True,
+            match=[matchers.fragment_identifier_matcher("test=1&foo=bar")],
+            body=b"test",
+        )
+
+        # two requests to check reversed order of fragment identifier
+        resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
+        assert_response(resp, "test")
+        resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
+        assert_response(resp, "test")
+
+    run()
+    assert_reset()
+
+
+def test_matchers_create_key_val_str():
+    """
+    Test that matchers._create_key_val_str does recursive conversion
+    """
+    data = {
+        "my_list": [
+            1,
+            2,
+            "a",
+            {"key1": "val1", "key2": 2, 3: "test"},
+            "!",
+            [["list", "nested"], {"nested": "dict"}],
+        ],
+        1: 4,
+        "test": "val",
+        "high": {"nested": "nested_dict"},
+    }
+    conv_str = matchers._create_key_val_str(data)
+    reference = (
+        "{1: 4, high: {nested: nested_dict}, my_list: [!, 1, 2, [[list, nested], {nested: dict}], "
+        "a, {3: test, key1: val1, key2: 2}], test: val}"
+    )
+    assert conv_str == reference
diff --git a/responses/test_responses.py b/responses/test_responses.py
index 9c02fe6..a909bd1 100644
--- a/responses/test_responses.py
+++ b/responses/test_responses.py
@@ -7,12 +7,11 @@ import os
 import re
 import six
 from io import BufferedReader, BytesIO
-from sys import version_info
 
 import pytest
 import requests
 import responses
-from requests.exceptions import ConnectionError, HTTPError
+from requests.exceptions import ConnectionError, HTTPError, ChunkedEncodingError
 from responses import (
     BaseResponse,
     Response,
@@ -343,28 +342,7 @@ def test_match_querystring():
     assert_reset()
 
 
-def test_query_string_matcher():
-    @responses.activate
-    def run():
-        url = "http://example.com?test=1&foo=bar"
-        responses.add(
-            responses.GET,
-            url,
-            body=b"test",
-            match=[matchers.query_string_matcher("test=1&foo=bar")],
-        )
-        resp = requests.get("http://example.com?test=1&foo=bar")
-        assert_response(resp, "test")
-        resp = requests.get("http://example.com?foo=bar&test=1")
-        assert_response(resp, "test")
-        resp = requests.get("http://example.com/?foo=bar&test=1")
-        assert_response(resp, "test")
-
-    run()
-    assert_reset()
-
-
-def test_match_empty_querystring():
+def test_match_querystring_empty():
     @responses.activate
     def run():
         responses.add(
@@ -669,6 +647,62 @@ def test_callback_content_type_dict():
     assert_reset()
 
 
+def test_callback_matchers():
+    def request_callback(request):
+        return (
+            200,
+            {"Content-Type": "application/json"},
+            b"foo",
+        )
+
+    @responses.activate
+    def run():
+        req_data = {"some": "other", "data": "fields"}
+        req_files = {"file_name": b"Old World!"}
+
+        responses.add_callback(
+            responses.POST,
+            url="http://httpbin.org/post",
+            match=[matchers.multipart_matcher(req_files, data=req_data)],
+            callback=request_callback,
+        )
+        resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files)
+        assert resp.text == "foo"
+        assert resp.headers["content-type"] == "application/json"
+
+    run()
+    assert_reset()
+
+
+def test_callback_matchers_fail():
+    @responses.activate
+    def run():
+        req_data = {"some": "other", "data": "fields"}
+        req_files = {"file_name": b"Old World!"}
+
+        responses.add_callback(
+            responses.POST,
+            url="http://httpbin.org/post",
+            match=[matchers.multipart_matcher(req_files, data=req_data)],
+            callback=lambda x: (
+                0,
+                {"a": ""},
+                "",
+            ),
+        )
+        with pytest.raises(ConnectionError) as exc:
+            requests.post(
+                "http://httpbin.org/post",
+                data={"some": "other", "data": "wrong"},
+                files=req_files,
+            )
+
+        assert "multipart/form-data doesn't match." in str(exc.value)
+
+    run()
+    assert_reset()
+
+
 def test_callback_content_type_tuple():
     def request_callback(request):
         return (
@@ -1177,6 +1211,43 @@ def test_headers():
     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():
@@ -1606,182 +1677,6 @@ def test_assert_call_count(url):
     assert_reset()
 
 
-def test_request_matches_post_params():
-    @responses.activate
-    def run(deprecated):
-        if deprecated:
-            json_params_matcher = getattr(responses, "json_params_matcher")
-            urlencoded_params_matcher = getattr(responses, "urlencoded_params_matcher")
-        else:
-            json_params_matcher = matchers.json_params_matcher
-            urlencoded_params_matcher = matchers.urlencoded_params_matcher
-
-        responses.add(
-            method=responses.POST,
-            url="http://example.com/",
-            body="one",
-            match=[json_params_matcher({"page": {"name": "first", "type": "json"}})],
-        )
-        responses.add(
-            method=responses.POST,
-            url="http://example.com/",
-            body="two",
-            match=[urlencoded_params_matcher({"page": "second", "type": "urlencoded"})],
-        )
-
-        resp = requests.request(
-            "POST",
-            "http://example.com/",
-            headers={"Content-Type": "x-www-form-urlencoded"},
-            data={"page": "second", "type": "urlencoded"},
-        )
-        assert_response(resp, "two")
-
-        resp = requests.request(
-            "POST",
-            "http://example.com/",
-            headers={"Content-Type": "application/json"},
-            json={"page": {"name": "first", "type": "json"}},
-        )
-        assert_response(resp, "one")
-
-    with pytest.deprecated_call():
-        run(deprecated=True)
-        assert_reset()
-
-    run(deprecated=False)
-    assert_reset()
-
-
-def test_request_matches_empty_body():
-    def run():
-        with responses.RequestsMock(assert_all_requests_are_fired=True) as rsps:
-            # test that both json and urlencoded body are empty in matcher and in request
-            rsps.add(
-                method=responses.POST,
-                url="http://example.com/",
-                body="one",
-                match=[matchers.json_params_matcher(None)],
-            )
-
-            rsps.add(
-                method=responses.POST,
-                url="http://example.com/",
-                body="two",
-                match=[matchers.urlencoded_params_matcher(None)],
-            )
-
-            resp = requests.request("POST", "http://example.com/")
-            assert_response(resp, "one")
-
-            resp = requests.request(
-                "POST",
-                "http://example.com/",
-                headers={"Content-Type": "x-www-form-urlencoded"},
-            )
-            assert_response(resp, "two")
-
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            # test exception raise if matcher body is None but request data is not None
-            rsps.add(
-                method=responses.POST,
-                url="http://example.com/",
-                body="one",
-                match=[matchers.json_params_matcher(None)],
-            )
-
-            with pytest.raises(ConnectionError) as excinfo:
-                resp = requests.request(
-                    "POST",
-                    "http://example.com/",
-                    json={"my": "data"},
-                    headers={"Content-Type": "application/json"},
-                )
-
-            msg = str(excinfo.value)
-            assert "request.body doesn't match: {my: data} doesn't match {}" in msg
-
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            rsps.add(
-                method=responses.POST,
-                url="http://example.com/",
-                body="two",
-                match=[matchers.urlencoded_params_matcher(None)],
-            )
-            with pytest.raises(ConnectionError) as excinfo:
-                resp = requests.request(
-                    "POST",
-                    "http://example.com/",
-                    headers={"Content-Type": "x-www-form-urlencoded"},
-                    data={"page": "second", "type": "urlencoded"},
-                )
-            msg = str(excinfo.value)
-            assert (
-                "request.body doesn't match: {page: second, type: urlencoded} doesn't match {}"
-                in msg
-            )
-
-    run()
-    assert_reset()
-
-
-def test_request_matches_params():
-    @responses.activate
-    def run():
-        url = "http://example.com/test"
-        params = {"hello": "world", "I am": "a big test"}
-        responses.add(
-            method=responses.GET,
-            url=url,
-            body="test",
-            match=[matchers.query_param_matcher(params)],
-            match_querystring=False,
-        )
-
-        # exchange parameter places for the test
-        params = {
-            "I am": "a big test",
-            "hello": "world",
-        }
-        resp = requests.get(url, params=params)
-
-        if six.PY3 and version_info[1] >= 7:
-            # only after py 3.7 dictionaries are ordered, so we can check URL
-            constructed_url = r"http://example.com/test?I+am=a+big+test&hello=world"
-            assert resp.url == constructed_url
-            assert resp.request.url == constructed_url
-
-        resp_params = getattr(resp.request, "params")
-        assert resp_params == params
-
-    run()
-    assert_reset()
-
-
-def test_multipart_matcher():
-    @responses.activate
-    def run():
-        req_data = {"some": "other", "data": "fields"}
-        req_files = {"file_name": b"Old World!"}
-        responses.add(
-            responses.POST,
-            url="http://httpbin.org/post",
-            match=[matchers.multipart_matcher(req_files, data=req_data)],
-        )
-        resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files)
-        assert resp.status_code == 200
-
-        with pytest.raises(TypeError):
-            responses.add(
-                responses.POST,
-                url="http://httpbin.org/post",
-                match=[matchers.multipart_matcher(files={})],
-            )
-
-    run()
-    assert_reset()
-
-
 def test_fail_request_error():
     """
     Validate that exception is raised if request URL/Method/kwargs don't match
@@ -1804,376 +1699,6 @@ def test_fail_request_error():
     assert_reset()
 
 
-def test_fail_matchers_error():
-    """
-    Validate that Exception is raised if request does not match responses.matchers
-        validate matchers.urlencoded_params_matcher
-        validate matchers.json_params_matcher
-        validate matchers.query_param_matcher
-        validate matchers.request_kwargs_matcher
-    :return: None
-    """
-
-    def run():
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            rsps.add(
-                "POST",
-                "http://example.com",
-                match=[matchers.urlencoded_params_matcher({"foo": "bar"})],
-            )
-            rsps.add(
-                "POST",
-                "http://example.com",
-                match=[matchers.json_params_matcher({"fail": "json"})],
-            )
-
-            with pytest.raises(ConnectionError) as excinfo:
-                requests.post("http://example.com", data={"id": "bad"})
-
-            msg = str(excinfo.value)
-            assert (
-                "request.body doesn't match: {id: bad} doesn't match {foo: bar}" in msg
-            )
-
-            assert (
-                "request.body doesn't match: JSONDecodeError: Cannot parse request.body"
-                in msg
-            )
-
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            rsps.add(
-                "GET",
-                "http://111.com",
-                match=[matchers.query_param_matcher({"my": "params"})],
-            )
-
-            rsps.add(
-                method=responses.GET,
-                url="http://111.com/",
-                body="two",
-                match=[matchers.json_params_matcher({"page": "one"})],
-            )
-
-            with pytest.raises(ConnectionError) as excinfo:
-                requests.get(
-                    "http://111.com", params={"id": "bad"}, json={"page": "two"}
-                )
-
-            msg = str(excinfo.value)
-            assert (
-                "Parameters do not match. {id: bad} doesn't match {my: params}" in msg
-            )
-            assert (
-                "request.body doesn't match: {page: two} doesn't match {page: one}"
-                in msg
-            )
-
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            req_kwargs = {
-                "stream": True,
-                "verify": False,
-            }
-            rsps.add(
-                "GET",
-                "http://111.com",
-                match=[matchers.request_kwargs_matcher(req_kwargs)],
-            )
-
-            with pytest.raises(ConnectionError) as excinfo:
-                requests.get("http://111.com", stream=True)
-
-            msg = str(excinfo.value)
-            assert (
-                "Arguments don't match: "
-                "{stream: True, verify: True} doesn't match {stream: True, verify: False}"
-            ) in msg
-
-    run()
-    assert_reset()
-
-
-def test_fail_multipart_matcher():
-    """
-    Validate that Exception is raised if request does not match responses.matchers
-        validate matchers.multipart_matcher
-    :return: None
-    """
-
-    def run():
-        # different file contents
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            req_data = {"some": "other", "data": "fields"}
-            req_files = {"file_name": b"Old World!"}
-            rsps.add(
-                responses.POST,
-                url="http://httpbin.org/post",
-                match=[matchers.multipart_matcher(req_files, data=req_data)],
-            )
-
-            with pytest.raises(ConnectionError) as excinfo:
-                requests.post(
-                    "http://httpbin.org/post",
-                    data=req_data,
-                    files={"file_name": b"New World!"},
-                )
-
-            msg = str(excinfo.value)
-            assert "multipart/form-data doesn't match. Request body differs." in msg
-            assert (
-                '\r\nContent-Disposition: form-data; name="file_name"; '
-                'filename="file_name"\r\n\r\nOld World!\r\n'
-            ) in msg
-            assert (
-                '\r\nContent-Disposition: form-data; name="file_name"; '
-                'filename="file_name"\r\n\r\nNew World!\r\n'
-            ) in msg
-
-        # x-www-form-urlencoded request
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            req_data = {"some": "other", "data": "fields"}
-            req_files = {"file_name": b"Old World!"}
-            rsps.add(
-                responses.POST,
-                url="http://httpbin.org/post",
-                match=[matchers.multipart_matcher(req_files, data=req_data)],
-            )
-
-            with pytest.raises(ConnectionError) as excinfo:
-                requests.post("http://httpbin.org/post", data=req_data)
-
-            msg = str(excinfo.value)
-            assert (
-                "multipart/form-data doesn't match. Request headers['Content-Type'] is different."
-                in msg
-            )
-            assert (
-                "application/x-www-form-urlencoded isn't equal to multipart/form-data; boundary="
-                in msg
-            )
-
-        # empty body request
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            req_files = {"file_name": b"Old World!"}
-            rsps.add(
-                responses.POST,
-                url="http://httpbin.org/post",
-                match=[matchers.multipart_matcher(req_files)],
-            )
-
-            with pytest.raises(ConnectionError) as excinfo:
-                requests.post("http://httpbin.org/post")
-
-            msg = str(excinfo.value)
-            assert "Request is missing the 'Content-Type' header" in msg
-
-    run()
-    assert_reset()
-
-
-def test_query_string_matcher_raises():
-    """
-    Validate that Exception is raised if request does not match responses.matchers
-        validate matchers.query_string_matcher
-            :return: None
-    """
-
-    def run():
-        with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps:
-            rsps.add(
-                "GET",
-                "http://111.com",
-                match=[matchers.query_string_matcher("didi=pro")],
-            )
-
-            with pytest.raises(ConnectionError) as excinfo:
-                requests.get("http://111.com", params={"test": "1", "didi": "pro"})
-
-            msg = str(excinfo.value)
-            assert (
-                "Query string doesn't match. {didi: pro, test: 1} doesn't match {didi: pro}"
-                in msg
-            )
-
-    run()
-    assert_reset()
-
-
-def test_request_matches_headers():
-    @responses.activate
-    def run():
-        url = "http://example.com/"
-        responses.add(
-            method=responses.GET,
-            url=url,
-            json={"success": True},
-            match=[matchers.header_matcher({"Accept": "application/json"})],
-        )
-
-        responses.add(
-            method=responses.GET,
-            url=url,
-            body="success",
-            match=[matchers.header_matcher({"Accept": "text/plain"})],
-        )
-
-        # the actual request can contain extra headers (requests always adds some itself anyway)
-        resp = requests.get(
-            url, headers={"Accept": "application/json", "Accept-Charset": "utf-8"}
-        )
-        assert_response(resp, body='{"success": true}', content_type="application/json")
-
-        resp = requests.get(url, headers={"Accept": "text/plain"})
-        assert_response(resp, body="success", content_type="text/plain")
-
-    run()
-    assert_reset()
-
-
-def test_request_matches_headers_no_match():
-    @responses.activate
-    def run():
-        url = "http://example.com/"
-        responses.add(
-            method=responses.GET,
-            url=url,
-            json={"success": True},
-            match=[matchers.header_matcher({"Accept": "application/json"})],
-        )
-
-        with pytest.raises(ConnectionError) as excinfo:
-            requests.get(url, headers={"Accept": "application/xml"})
-
-        msg = str(excinfo.value)
-        assert (
-            "Headers do not match: {Accept: application/xml} doesn't match "
-            "{Accept: application/json}"
-        ) in msg
-
-    run()
-    assert_reset()
-
-
-def test_request_matches_headers_strict_match():
-    @responses.activate
-    def run():
-        url = "http://example.com/"
-        responses.add(
-            method=responses.GET,
-            url=url,
-            body="success",
-            match=[
-                matchers.header_matcher({"Accept": "text/plain"}, strict_match=True)
-            ],
-        )
-
-        # requests will add some extra headers of its own, so we have to use prepared requests
-        session = requests.Session()
-
-        # make sure we send *just* the header we're expectin
-        prepped = session.prepare_request(
-            requests.Request(
-                method="GET",
-                url=url,
-            )
-        )
-        prepped.headers.clear()
-        prepped.headers["Accept"] = "text/plain"
-
-        resp = session.send(prepped)
-        assert_response(resp, body="success", content_type="text/plain")
-
-        # include the "Accept-Charset" header, which will fail to match
-        prepped = session.prepare_request(
-            requests.Request(
-                method="GET",
-                url=url,
-            )
-        )
-        prepped.headers.clear()
-        prepped.headers["Accept"] = "text/plain"
-        prepped.headers["Accept-Charset"] = "utf-8"
-
-        with pytest.raises(ConnectionError) as excinfo:
-            session.send(prepped)
-
-        msg = str(excinfo.value)
-        assert (
-            "Headers do not match: {Accept: text/plain, Accept-Charset: utf-8} "
-            "doesn't match {Accept: text/plain}"
-        ) in msg
-
-    run()
-    assert_reset()
-
-
-def test_fragment_identifier_matcher():
-    @responses.activate
-    def run():
-        responses.add(
-            responses.GET,
-            "http://example.com",
-            match=[matchers.fragment_identifier_matcher("test=1&foo=bar")],
-            body=b"test",
-        )
-
-        resp = requests.get("http://example.com#test=1&foo=bar")
-        assert_response(resp, "test")
-
-    run()
-    assert_reset()
-
-
-def test_fragment_identifier_matcher_error():
-    @responses.activate
-    def run():
-        responses.add(
-            responses.GET,
-            "http://example.com/",
-            match=[matchers.fragment_identifier_matcher("test=1")],
-        )
-        responses.add(
-            responses.GET,
-            "http://example.com/",
-            match=[matchers.fragment_identifier_matcher(None)],
-        )
-
-        with pytest.raises(ConnectionError) as excinfo:
-            requests.get("http://example.com/#test=2")
-
-        msg = str(excinfo.value)
-        assert (
-            "URL fragment identifier is different: test=1 doesn't match test=2"
-        ) in msg
-        assert (
-            "URL fragment identifier is different: None doesn't match test=2"
-        ) in msg
-
-    run()
-    assert_reset()
-
-
-def test_fragment_identifier_matcher_and_match_querystring():
-    @responses.activate
-    def run():
-        url = "http://example.com?ab=xy&zed=qwe#test=1&foo=bar"
-        responses.add(
-            responses.GET,
-            url,
-            match_querystring=True,
-            match=[matchers.fragment_identifier_matcher("test=1&foo=bar")],
-            body=b"test",
-        )
-
-        # two requests to check reversed order of fragment identifier
-        resp = requests.get("http://example.com?ab=xy&zed=qwe#test=1&foo=bar")
-        assert_response(resp, "test")
-        resp = requests.get("http://example.com?zed=qwe&ab=xy#foo=bar&test=1")
-        assert_response(resp, "test")
-
-    run()
-    assert_reset()
-
-
 @pytest.mark.parametrize(
     "response_params, expected_representation",
     [
@@ -2267,28 +1792,3 @@ def test_rfc_compliance(url, other_url):
 
     run()
     assert_reset()
-
-
-def test_matchers_create_key_val_str():
-    """
-    Test that matchers._create_key_val_str does recursive conversion
-    """
-    data = {
-        "my_list": [
-            1,
-            2,
-            "a",
-            {"key1": "val1", "key2": 2, 3: "test"},
-            "!",
-            [["list", "nested"], {"nested": "dict"}],
-        ],
-        1: 4,
-        "test": "val",
-        "high": {"nested": "nested_dict"},
-    }
-    conv_str = matchers._create_key_val_str(data)
-    reference = (
-        "{1: 4, high: {nested: nested_dict}, my_list: [!, 1, 2, [[list, nested], {nested: dict}], "
-        "a, {3: test, key1: val1, key2: 2}], test: val}"
-    )
-    assert conv_str == reference