Skip to content

Add opt-in case-insensitive header value matcher - #524

Merged
csernazs merged 2 commits into
csernazs:masterfrom
00200200:codex/match-header-names-case-insensitively
Oct 14, 2026
Merged

csernazs merged 2 commits into
csernazs:masterfrom
00200200:codex/match-header-names-case-insensitively

Conversation

@00200200

@00200200 00200200 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Reworked following the review to provide a separate, opt-in matcher rather than change the existing class.

Approach

  • Restore HeaderValueMatcher's exact-name lookup and existing defaults.
  • Add CaseInsensitiveHeaderValueMatcher, selected explicitly through header_value_matcher or bake().
  • Normalize comparator keys once at construction and use direct dictionary lookup. Each instance owns its registry; it neither reads nor modifies HeaderValueMatcher.DEFAULT_MATCHERS.
  • Keep ordinary header values case-sensitive and retain the Authorization parser. A supplied mapping replaces built-in comparators; duplicate normalized names use the last entry, and later mapping changes require a new instance. A defaultdict fallback does not cache missing names.
  • Export the new class, accept its callable interface in BlockingHTTPServer, and document opt-in usage.

Tests cover Digest header-name casing, custom comparator names, fallback behavior, duplicate keys, instance isolation, legacy behavior, and both regular and blocking HTTP servers.

Validation

On macOS with Python 3.12.11:

  • Focused header and blocking-server tests: 43 passed.
  • Full suite including release checks: 171 passed, 3 skipped, 2 xfailed.
  • SSL selection: 1 passed, 175 skipped.
  • Mypy: 57 files checked successfully. Ruff, Black, isort and git diff --check passed.
  • Sphinx HTML build succeeded. Both the previous PR head and the patch produce the same 59 warnings after normalizing paths and line numbers; no new documentation warnings. A warnings-as-errors build is not clean.

Other operating systems and Python versions, and the full pinned pre-commit environment, were not run locally. Prepared and tested with Codex.

@csernazs

Copy link
Copy Markdown
Owner

hi @00200200,

First of all, thanks for your PR!

In its current state, HeaderValueMatcher expects the case sensitive based header name matching. I think adding case insensitive to this class is overkill, for example it uses a dict to make the lookup faster, then in order to make that dict case-insensitive your PR iterates over the keys. Also, this class already have an issue about crosstalk between the test so I'd stop touching this.

I see two solution for your issue (I agree that header matching should be case-insensitive, no question in this).

One solution is to implement a totally new HeaderValueMatcher class for case-insensitive case (no pun intended :)), this would live separately and initially users like you would need to specify it explicitly. Once it is working fine then we could possibly flip the default to this new class.

pytest-httpserver is designed to be fully customizable so hence I'd suggest creating a new class.

The other solution is just simply add a few lines to your tests.

from pytest_httpserver import HeaderValueMatcher

def header_matcher(header_name: str, actual: str | None, expected: str) -> bool:
    if header_name.lower() == "authorization":
        return HeaderValueMatcher.authorization_header_value_matcher(actual, expected)

    return actual == expected

Then you can use it like this, we even provide the bake method now which makes test writing more simpler:

@pytest.mark.parametrize(
    "header_name",
    [
        "foo",
        "Foo",
        "FOO",
        "FoO",
    ],
)
def test_case_insensitive_header_matcher(httpserver: HTTPServer, header_name: str):
    server = httpserver.bake(header_value_matcher=header_matcher)
    server.expect_request("/foo", headers={"Foo": "bar"}).respond_with_data("OK")

    assert requests.get(server.url_for("/foo"), headers={header_name: "bar"}).text == "OK"

And it will also work with the authorization header in a case insensitive way:

def test_authorization_headers(httpserver: HTTPServer):
    server = httpserver.bake(header_value_matcher=header_matcher)
    headers_with_values_in_direct_order = {
        "AuTHoRIZation": (
            'Digest username="Mufasa",'
            'realm="testrealm@host.com",'
            'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",'
            'uri="/dir/index.html",'
            "qop=auth,"
            "nc=00000001,"
            'cnonce="0a4f113b",'
            'response="6629fae49393a05397450978507c4ef1",'
            'opaque="5ccc069c403ebaf9f0171e9517f40e41"'
        )
    }
    server.expect_request(uri="/", headers=headers_with_values_in_direct_order).respond_with_data("OK")
    response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_direct_order)
    assert response.status_code == 200
    assert response.text == "OK"

note that I added 1 line to the original authorization test

I think this itself is much cleaner solution than your PR. In addition to that: you have full authority how you do the comparison in that function.

What do you think?

Zsolt

@00200200 00200200 changed the title Match header comparator names case-insensitively Add opt-in case-insensitive header value matcher Oct 14, 2026
@csernazs
csernazs force-pushed the codex/match-header-names-case-insensitively branch from 542a6bc to 6fc4ef9 Compare October 14, 2026 20:33
@csernazs
csernazs merged commit 84f2894 into csernazs:master Oct 14, 2026
10 checks passed
@csernazs

Copy link
Copy Markdown
Owner

Thanks for your PR, I've just merge it.

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