Repository: getsentry/responses

Issue:
multipart_matcher doesn't work with non-UTF-8 bytes
### Environment

`responses==0.16.0`, `python==3.8.12`

### Steps to Reproduce

```python
import requests
import responses
from responses.matchers import multipart_matcher

non_utf_bytes = b'\xacHello World!'

def post_non_utf_bytes():
    requests.post(url="https://httpbin.org/post", files={"file": non_utf_bytes})

@responses.activate
def test_non_utf_8():
    responses.add(
        method=responses.POST,
        url="https://httpbin.org/post",
        match=[
            multipart_matcher(
                files={"file": non_utf_bytes},
            )
        ],
    )
    post_non_utf_bytes()

if __name__ == '__main__':
    post_non_utf_bytes()  # works, allowed
    test_non_utf_8()  # fails, tries to decode rather than doing byte-byte comparison
```

### Expected Result

As the `post` itself is valid I'd expect the body comparison to be done as bytestring-bytestring comparison rather than trying to decode it.

### Actual Result

```
responses/matchers.py", line 279, in match
    request_body = request_body.decode("utf-8")
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xac in position 100: invalid start byte
```


Discussion:
codecov[bot]: # [Codecov](https://app.codecov.io/gh/getsentry/responses/compare/444?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
> Merging [#444](https://app.codecov.io/gh/getsentry/responses/compare/444?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (4ede9ea) into [master](https://codecov.io/gh/getsentry/responses/commit/8a5a4b6aac026eadaddb2610eef9b9e17ee9d0f3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) (8a5a4b6) will **decrease** coverage by `1.39%`.
> The diff coverage is `81.81%`.

[![Impacted file tree graph](https://codecov.io/gh/getsentry/responses/pull/444/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/444?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry)

```diff
@@             Coverage Diff             @@
##            master     #444      +/-   ##
===========================================
- Coverage   100.00%   98.60%   -1.40%     
===========================================
  Files            4        4              
  Lines         1930     1933       +3     
===========================================
- Hits          1930     1906      -24     
- Misses           0       27      +27     
```


| [Impacted Files](https://app.codecov.io/gh/getsentry/responses/compare/444?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) | Coverage Δ | |
|---|---|---|
| [responses/test\_matchers.py](https://app.codecov.io/gh/getsentry/responses/compare/444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RfbWF0Y2hlcnMucHk=) | `99.20% <77.77%> (-0.80%)` | :arrow_down: |
| [responses/matchers.py](https://app.codecov.io/gh/getsentry/responses/compare/444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL21hdGNoZXJzLnB5) | `97.98% <100.00%> (-2.02%)` | :arrow_down: |
| [responses/\_\_init\_\_.py](https://app.codecov.io/gh/getsentry/responses/compare/444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL19faW5pdF9fLnB5) | `95.57% <0.00%> (-4.43%)` | :arrow_down: |
| [responses/test\_responses.py](https://app.codecov.io/gh/getsentry/responses/compare/444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry#diff-cmVzcG9uc2VzL3Rlc3RfcmVzcG9uc2VzLnB5) | `99.81% <0.00%> (-0.19%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://app.codecov.io/gh/getsentry/responses/compare/444?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/444?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry). Last update [8a5a4b6...4ede9ea](https://app.codecov.io/gh/getsentry/responses/compare/444?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 fa4aeb5..6085c46 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,6 +4,7 @@
 * 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
+* Added support for non-UTF-8 bytes in `responses.matchers.multipart_matcher`
 
 0.16.0
 ------
diff --git a/responses/matchers.py b/responses/matchers.py
index f90edef..1c4ef35 100644
--- a/responses/matchers.py
+++ b/responses/matchers.py
@@ -275,12 +275,12 @@ def multipart_matcher(files, data=None):
         )
 
         request_body = request.body
-        if isinstance(request_body, bytes):
-            request_body = request_body.decode("utf-8")
-
         prepared_body = prepared.body
+
         if isinstance(prepared_body, bytes):
-            prepared_body = prepared_body.decode("utf-8")
+            # since headers always come as str, need to convert to bytes
+            prepared_boundary = prepared_boundary.encode("utf-8")
+            request_boundary = request_boundary.encode("utf-8")
 
         prepared_body = prepared_body.replace(prepared_boundary, request_boundary)
 


Test patch:
diff --git a/responses/test_matchers.py b/responses/test_matchers.py
index ba6afb3..4587b28 100644
--- a/responses/test_matchers.py
+++ b/responses/test_matchers.py
@@ -283,17 +283,32 @@ def test_fail_matchers_error():
     assert_reset()
 
 
-def test_multipart_matcher():
+@pytest.mark.parametrize(
+    "req_file,match_file",
+    [
+        (b"Old World!", "Old World!"),
+        ("Old World!", b"Old World!"),
+        (b"Old World!", b"Old World!"),
+        ("Old World!", "Old World!"),
+        (b"\xacHello World!", b"\xacHello World!"),
+    ],
+)
+def test_multipart_matcher(req_file, match_file):
     @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)],
+            match=[
+                matchers.multipart_matcher(
+                    files={"file_name": match_file}, data=req_data
+                )
+            ],
+        )
+        resp = requests.post(
+            "http://httpbin.org/post", data=req_data, files={"file_name": req_file}
         )
-        resp = requests.post("http://httpbin.org/post", data=req_data, files=req_files)
         assert resp.status_code == 200
 
         with pytest.raises(TypeError):
@@ -334,14 +349,24 @@ def test_multipart_matcher_fail():
 
             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
+            if six.PY2:
+                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
+            else:
+                assert (
+                    r'\r\nContent-Disposition: form-data; name="file_name"; '
+                    r'filename="file_name"\r\n\r\nOld World!\r\n'
+                ) in msg
+                assert (
+                    r'\r\nContent-Disposition: form-data; name="file_name"; '
+                    r'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: