Skip to content

Match header field names case-insensitively in the default header_matcher - #805

Open
chuenchen309 wants to merge 2 commits into
getsentry:masterfrom
chuenchen309:fix/header-matcher-case-insensitive
Open

Match header field names case-insensitively in the default header_matcher#805
chuenchen309 wants to merge 2 commits into
getsentry:masterfrom
chuenchen309:fix/header-matcher-case-insensitive

Conversation

@chuenchen309

@chuenchen309 chuenchen309 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

HTTP header field names are case-insensitive, and requests stores request.headers as a CaseInsensitiveDict. But the default header_matcher (strict_match=False) rebuilds them into a plain dict keyed by the request's original casing, filtered with a case-sensitive k in headers:

request_headers = {k: v for k, v in request_headers.items() if k in headers}

So any casing difference drops the header and the match fails — with a misleading {} doesn't match reason:

matchers.header_matcher({"X-Custom": "foo"})   # request sends "x-custom: foo"
# -> (False, "Headers do not match: {} doesn't match {'X-Custom': 'foo'}")

strict_match=True keeps the CaseInsensitiveDict and already matches case-insensitively, so the default path was the odd one out. It matters in practice because HTTP/2 mandates lowercase header names, so client code (or a recorded cassette) can easily send content-type where the matcher spec writes Content-Type.

The fix keys the filtered dict by the matcher's names, using CaseInsensitiveDict membership so the case-insensitive lookup survives. Value comparisons, missing-header detection, and strict_match=True are unchanged; the full suite stays green.

Disclosure: found and written by an AI coding agent (Claude Code) running autonomously on this account; the human account holder reviews every change and is accountable for it. The verification is real and re-runnable from the diff.

…cher

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) <noreply@anthropic.com>
@chuenchen309
chuenchen309 requested a review from markstory as a code owner July 18, 2026 21:15
Comment on lines +873 to +874
resp = requests.get(url, headers={"x-custom": "token"})
assert_response(resp, body='{"success": true}', content_type="application/json")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could there also be a test that covers case-mismatch on strict_match=True? There isn't a test for that scenario right now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants