From 4c118559cabb5bdbdf57f4bba90b5f2bc748dfd5 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 05:15:18 +0800 Subject: [PATCH 1/2] Match header field names case-insensitively in the default header_matcher HTTP header names are case-insensitive and requests stores request.headers as a CaseInsensitiveDict, but the default (strict_match=False) path rebuilt them into a plain dict keyed by the request's original casing with a case-sensitive `k in headers` filter. Any casing difference dropped the header, so header_matcher({"X-Custom": ...}) failed to match a request sending "x-custom" (and vice versa), with a misleading "{} doesn't match" reason. strict_match=True kept the CaseInsensitiveDict and already matched case-insensitively, so the default path was the outlier. Key the filtered dict by the matcher's names using CaseInsensitiveDict membership so the case-insensitive lookup survives. Co-Authored-By: Claude Opus 4.8 (1M context) --- responses/matchers.py | 8 ++++++-- responses/tests/test_matchers.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/responses/matchers.py b/responses/matchers.py index d61decc1..5c2e0fb3 100644 --- a/responses/matchers.py +++ b/responses/matchers.py @@ -432,8 +432,12 @@ def match(request: PreparedRequest) -> Tuple[bool, str]: request_headers: Union[Mapping[Any, Any], Any] = request.headers or {} if not strict_match: - # filter down to just the headers specified in the matcher - request_headers = {k: v for k, v in request_headers.items() if k in headers} + # Filter to the matcher's headers, keyed by the matcher's names, so + # the case-insensitive lookup on request.headers (a + # CaseInsensitiveDict) is not lost in the plain-dict rebuild. + request_headers = { + k: request_headers[k] for k in headers if k in request_headers + } valid = _compare_with_regex(request_headers) diff --git a/responses/tests/test_matchers.py b/responses/tests/test_matchers.py index fc4fbc6f..5b56838b 100644 --- a/responses/tests/test_matchers.py +++ b/responses/tests/test_matchers.py @@ -857,6 +857,26 @@ def run(): assert_reset() +def test_request_matches_headers_case_insensitive_field_names(): + # HTTP header names are case-insensitive (and HTTP/2 lower-cases them), so + # the default matcher must match regardless of the field-name casing. + @responses.activate + def run(): + url = "http://example.com/" + responses.add( + method=responses.GET, + url=url, + json={"success": True}, + match=[matchers.header_matcher({"X-Custom": "token"})], + ) + + resp = requests.get(url, headers={"x-custom": "token"}) + assert_response(resp, body='{"success": true}', content_type="application/json") + + run() + assert_reset() + + def test_request_header_value_mismatch_raises(): @responses.activate def run(): From 8da842acc4e4bfe14167552273f53905607bccf9 Mon Sep 17 00:00:00 2001 From: Andrew Chen <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 05:16:22 +0800 Subject: [PATCH 2/2] Add CHANGES entry for the header_matcher case-insensitivity fix --- CHANGES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index dcdce261..1ec7c55a 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,10 @@ registered URL's own query string) discarding blank-valued query params (``b=``), which caused requests with an extra or missing blank param to match incorrectly. See #804 +* Fixed the default `header_matcher` (``strict_match=False``) matching header + field names case-sensitively, so a request whose header casing differed from + the matcher spec (for example lowercase names over HTTP/2) failed to match. + See #805 0.26.2 ------