simahawk: travis red on some combinations as `master`. Apparently not related to this PR.
neozenith: Changes look good. Thanks for the PR!

You’re right, there is something on master CI we haven’t got around to addressing that’s throwing some red. 

When I get some spare time I’ll have a look to clear that so we can push this through. 

If you could add the associated test that’d be appreciated. 

And.... add in `docs/Changelog.rst` a line to claim credit. 
neozenith: Turns out the CI red is due to a Yarl version issue raised in #485 
neozenith: Fixed in #489, please rebase with master and let's have another crack at getting this pushed through. Due to release to PyPI the accumulated patches too.
simahawk: @neozenith rebased, tests updated
simahawk: forgot to run black. Any reason not to use https://pre-commit.com/ here?
codecov-io: # [Codecov](https://codecov.io/gh/kevin1024/vcrpy/pull/488?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@d843d2a`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/kevin1024/vcrpy/pull/488/graphs/tree.svg?width=650&token=YOLqUk37fb&height=150&src=pr)](https://codecov.io/gh/kevin1024/vcrpy/pull/488?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##             master     #488   +/-   ##
=========================================
  Coverage          ?   89.41%           
=========================================
  Files             ?       24           
  Lines             ?     1549           
  Branches          ?      216           
=========================================
  Hits              ?     1385           
  Misses            ?      135           
  Partials          ?       29
```


| [Impacted Files](https://codecov.io/gh/kevin1024/vcrpy/pull/488?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [vcr/matchers.py](https://codecov.io/gh/kevin1024/vcrpy/pull/488/diff?src=pr&el=tree#diff-dmNyL21hdGNoZXJzLnB5) | `96% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kevin1024/vcrpy/pull/488?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kevin1024/vcrpy/pull/488?src=pr&el=footer). Last update [d843d2a...f7457d9](https://codecov.io/gh/kevin1024/vcrpy/pull/488?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/docs/changelog.rst b/docs/changelog.rst
index e728107..bda16ba 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -7,6 +7,7 @@ Changelog
   - Fix exception when body is empty (@keithprickett)
   - Add `pytest-recording` to the documentation as an alternative Pytest plugin (@Stranger6667)
   - Fix yarl and python3.5 version issue (@neozenith)
+  - Fix header matcher for boto3 - fixes #474 (@simahawk)
 -  2.1.0 - Add a `rewind` method to reset a cassette (thanks @khamidou)
    New error message with more details on why the cassette failed to play a request (thanks @arthurHamon2, @neozenith)
    Handle connect tunnel URI (thanks @jeking3)
diff --git a/vcr/matchers.py b/vcr/matchers.py
index 7a969f4..eabd61f 100644
--- a/vcr/matchers.py
+++ b/vcr/matchers.py
@@ -53,7 +53,10 @@ def headers(r1, r2):
 
 def _header_checker(value, header="Content-Type"):
     def checker(headers):
-        return value in headers.get(header, "").lower()
+        _header = headers.get(header, "")
+        if isinstance(_header, bytes):
+            _header = _header.decode("utf-8")
+        return value in _header.lower()
 
     return checker
 

TypeError: a bytes-like object is required, not 'str' when processing header object
```
headers = {'X-Amz-Target': b'<target>', 'Content-Type': b'<content-type>', 'User-Agent': b'<user-agent>', Signature=b'<sig>', 'Content-Length': '<len>'}

    def checker(headers):
>       return value in headers.get(header, '').lower()
E       TypeError: a bytes-like object is required, not 'str'

.venv/lib/python3.6/site-packages/vcr/matchers.py:56: TypeError
```

The code on line 56 in matchers fails unexpectedly. The content type return as a bytes like object   but the code still fails. When we convert the bytes object to a string it works. Replacing the current function at https://github.com/kevin1024/vcrpy/blob/master/vcr/matchers.py#L54-L57:
```
def _header_checker(value, header='Content-Type'):
    def checker(headers):
        return value in headers.get(header, '').lower()
    return checker
```
with  
```
def _header_checker(value, header='Content-Type'):
    def checker(headers):
        return value in headers.get(header, '').decode('utf-8').lower()
    return checker
```
fixes this issue especially with boto3 requests. 

Would there be any risk with making this change?
diff --git a/tests/unit/test_matchers.py b/tests/unit/test_matchers.py
index 4d161ca..34eadd3 100644
--- a/tests/unit/test_matchers.py
+++ b/tests/unit/test_matchers.py
@@ -54,6 +54,16 @@ req2_body = (
     b"<member><name>a</name><value><string>1</string></value></member>"
     b"</struct></value></data></array></value></param></params></methodCall>"
 )
+boto3_bytes_headers = {
+    "X-Amz-Content-SHA256": b"UNSIGNED-PAYLOAD",
+    "Cache-Control": b"max-age=31536000, public",
+    "X-Amz-Date": b"20191102T143910Z",
+    "User-Agent": b"Boto3/1.9.102 Python/3.5.3 Linux/4.15.0-54-generic Botocore/1.12.253 Resource",
+    "Content-MD5": b"GQqjEXsRqrPyxfTl99nkAg==",
+    "Content-Type": b"text/plain",
+    "Expect": b"100-continue",
+    "Content-Length": "21",
+}
 
 
 @pytest.mark.parametrize(
@@ -110,6 +120,11 @@ req2_body = (
                 "POST", "http://host.com/", '{"b": 2, "a": 1}', {"content-type": "application/json"}
             ),
         ),
+        (
+            # special case for boto3 bytes headers
+            request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
+            request.Request("POST", "http://aws.custom.com/", b"123", boto3_bytes_headers),
+        ),
     ],
 )
 def test_body_matcher_does_match(r1, r2):

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-httpbin pytest-asyncio
apt-get update apt-get install -y libyaml-dev
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==24.2.0
brotlicffi==1.1.0.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
click==8.1.8
coverage==7.2.7
decorator==5.1.1
exceptiongroup==1.2.2
flasgger==0.9.7.1
Flask==2.2.5
greenlet==2.0.2
httpbin==0.10.2
idna==3.10
importlib-metadata==6.7.0
importlib-resources==5.12.0
iniconfig==2.0.0
itsdangerous==2.1.2
Jinja2==3.1.6
jsonschema==4.17.3
MarkupSafe==2.1.5
mistune==3.0.2
multidict==6.0.5
packaging==24.0
pkgutil_resolve_name==1.3.10
pluggy==1.2.0
pycparser==2.21
pyrsistent==0.19.3
pytest==7.4.4
pytest-asyncio==0.21.2
pytest-cov==4.1.0
pytest-httpbin==2.0.0
PyYAML==6.0.1
six==1.17.0
tomli==2.0.1
typing_extensions==4.7.1
-e git+https://github.com/kevin1024/vcrpy.git@d843d2a6c1f26aad83bc35fb2f6dcb8d2d3c4989#egg=vcrpy
Werkzeug==2.2.3
wrapt==1.16.0
yarl==1.9.4
zipp==3.15.0

name: vcrpy
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==24.2.0
      - brotlicffi==1.1.0.0
      - cffi==1.15.1
      - click==8.1.8
      - coverage==7.2.7
      - decorator==5.1.1
      - exceptiongroup==1.2.2
      - flasgger==0.9.7.1
      - flask==2.2.5
      - greenlet==2.0.2
      - httpbin==0.10.2
      - idna==3.10
      - importlib-metadata==6.7.0
      - importlib-resources==5.12.0
      - iniconfig==2.0.0
      - itsdangerous==2.1.2
      - jinja2==3.1.6
      - jsonschema==4.17.3
      - markupsafe==2.1.5
      - mistune==3.0.2
      - multidict==6.0.5
      - packaging==24.0
      - pkgutil-resolve-name==1.3.10
      - pluggy==1.2.0
      - pycparser==2.21
      - pyrsistent==0.19.3
      - pytest==7.4.4
      - pytest-asyncio==0.21.2
      - pytest-cov==4.1.0
      - pytest-httpbin==2.0.0
      - pyyaml==6.0.1
      - six==1.17.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - vcrpy==2.1.0
      - werkzeug==2.2.3
      - wrapt==1.16.0
      - yarl==1.9.4
      - zipp==3.15.0
prefix: /opt/conda/envs/vcrpy

tests/unit/test_matchers.py::test_body_matcher_does_match[r17-r27]
tests/unit/test_matchers.py::test_uri_matcher tests/unit/test_matchers.py::test_body_matcher_does_match[r10-r20] tests/unit/test_matchers.py::test_body_matcher_does_match[r11-r21] tests/unit/test_matchers.py::test_body_matcher_does_match[r12-r22] tests/unit/test_matchers.py::test_body_matcher_does_match[r13-r23] tests/unit/test_matchers.py::test_body_matcher_does_match[r14-r24] tests/unit/test_matchers.py::test_body_matcher_does_match[r15-r25] tests/unit/test_matchers.py::test_body_matcher_does_match[r16-r26] tests/unit/test_matchers.py::test_body_match_does_not_match[r10-r20] tests/unit/test_matchers.py::test_body_match_does_not_match[r11-r21] tests/unit/test_matchers.py::test_body_match_does_not_match[r12-r22] tests/unit/test_matchers.py::test_query_matcher tests/unit/test_matchers.py::test_matchers tests/unit/test_matchers.py::test_evaluate_matcher_does_match tests/unit/test_matchers.py::test_evaluate_matcher_does_not_match tests/unit/test_matchers.py::test_evaluate_matcher_does_not_match_with_assert_message tests/unit/test_matchers.py::test_get_assertion_message tests/unit/test_matchers.py::test_get_assertion_message_with_details tests/unit/test_matchers.py::test_get_matchers_results[r10-r20-expected_successes0-expected_failures0] tests/unit/test_matchers.py::test_get_matchers_results[r11-r21-expected_successes1-expected_failures1] tests/unit/test_matchers.py::test_get_matchers_results[r12-r22-expected_successes2-expected_failures2] tests/unit/test_matchers.py::test_requests_match[successes0-failures0-True] tests/unit/test_matchers.py::test_requests_match[successes1-failures1-False] tests/unit/test_matchers.py::test_requests_match[successes2-failures2-False]
docs/changelog.rst vcr/matchers.py
docs/changelog.rst vcr/matchers.py