Skip to content

feat(ext): make HeaderCaseMap public - #4203

Open
Runnin4ik wants to merge 1 commit into
hyperium:masterfrom
Runnin4ik:ext-public-header-case-map
Open

Runnin4ik wants to merge 1 commit into
hyperium:masterfrom
Runnin4ik:ext-public-header-case-map

Conversation

@Runnin4ik

@Runnin4ik Runnin4ik commented Sep 25, 2026 •

Copy link
Copy Markdown

Exposes hyper::ext::HeaderCaseMap, which was pub(crate), so that a client can write an HTTP/1.1 request with the original casing of its header names.

http::HeaderName lowercases the name, and hyper's HTTP/1 encoder writes that lowercased name, so a client that has to speak to a server which compares the bytes it receives had no supported way to do it. That is the fingerprinting/impersonation case: the request has to carry what a browser sends, down to the casing, e.g. TE: trailers, or a lowercased sec-ch-ua right next to a title-cased Sec-Fetch-Site. HeaderCaseMap already holds exactly this information for messages hyper itself parsed with preserve_header_case; this makes the type public and gives it a public way to be built, so it can be put into a request's extensions:

let mut case_map = HeaderCaseMap::default();
case_map.append(header::TE, Bytes::from_static(b"TE"));
req.extensions_mut().insert(case_map);

The encoder writes the spellings the map holds, and falls back to the lowercased HeaderName for a name it does not mention, which is the behavior it already had for messages hyper parsed.

Scope notes:

  • append is the minimal public constructor: the lowercased HeaderName to look the entry up by, and the Bytes to write. Its documentation states that the bytes must spell that name, since they go to the wire verbatim. There is deliberately no public read access yet — that half of the design (cased-name types, validation, round-tripping, as discussed on Original Header Cases API #2695) is still open, and can be added later without changing what is here.
  • default() moves out of the inherent impl into a public impl Default, so the empty map is built the idiomatic way. insert — the writer the parser and the C API use — stays pub(crate), which leaves append as the only public constructor.
  • Additive only: nothing public changes shape, so there is no breaking change here.
  • Tests: a unit test that two spellings differing only in case stay distinct entries under the one lowercased name, and an encoder test that a request built with a public map round-trips its casing through the HTTP/1 client encoder.

Refs #2695. This is the minimal exposure suggested in #2695 (comment).

Related: #3971 ("Add header casing public API") explores the same design space from the other direction — it switches the map's value type from Bytes to String for validation, but leaves the type and all of its methods pub(crate), so it does not expose anything yet — and is parked behind the proposal process that #4131 (HIP-0001, merged) set up. If that work is the better path, I am happy to rebase this behind it or close it; this is posted as the minimal-methods version to have something concrete to review on the implementation side.

Checked locally on this branch, since GitHub reports no checks on it (the first-time-contributor approval gate rather than a failure): cargo test --features full — 117 unit tests and 15 doctests pass, 0 failed, the two tests above and the documentation example among them; cargo fmt --check clean.

User-facing asks this answers: #1492 ("Headers are lower-cased when sent and no option to disable this feature").

`HeaderCaseMap` was internal, so a message could only carry the original
casing of names hyper itself had parsed, and only with
`preserve_header_case` turned on. A client that has to write a name the way
some other program writes it, because a server compares the bytes it
receives, had no way to say so: `http::HeaderName` lowercases the name, and
the HTTP/1 encoder writes that lowercased name, so the request no longer
matches the client being imitated. Names such as `TE` next to a lowercased
`sec-ch-ua`, or a title-cased `Sec-Fetch-Site`, are sent in the casing a
browser sends them by.

The type is public now, with a `Default` impl and a public `append` that
records the spelling a name was written with. Putting the map into a
message's extensions makes the HTTP/1 encoder write the spellings it holds,
and write the lowercased `HeaderName` for a name the map does not mention,
which is the behavior it already had for messages hyper parsed.

No public way to read the spellings back is added here; that part of the
design is still open.

Refs: hyperium#2695
@Runnin4ik
Runnin4ik force-pushed the ext-public-header-case-map branch from 96d4e2e to a04dd36 Compare September 25, 2026 01:47

This branch has not been deployed

No deployments
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.

1 participant