A fork of the hyperium/http crate that preserves the original casing of CUSTOM HTTP header names.
Some legacy servers require HTTP headers with specific casing (e.g., X-Custom-Header instead of x-custom-header). The standard http crate normalizes all header names to lowercase, which breaks compatibility with such servers.
This fork modifies the http crate to:
- Preserve original casing for custom headers (e.g.,
X-Custom-Headerstays as-is) - Maintain case-insensitive comparison for Hash/Eq (required for correct
HeaderMapbehavior) - Stay compatible with
reqwest,hyper,h2, and the broader Rust HTTP ecosystem
| Header Type | as_str() Output | Notes |
|---|---|---|
| Custom headers | Original casing preserved | X-Custom-Header → "X-Custom-Header" |
| Standard headers | Lowercase | Content-Type → "content-type" |
Standard headers (Content-Type, Accept, etc.) are normalized to lowercase to maintain compatibility with the h2 crate, which uses HeaderName constants in match patterns. This requires StructuralPartialEq, which is only possible with derived (not manual) PartialEq.
For most use cases, this is acceptable because:
- RFC 7230 requires HTTP headers to be case-insensitive
- Custom headers (
X-*or non-standard names) are typically where case sensitivity matters for legacy systems
To use this fork with crates like reqwest that depend on http:
[dependencies]
reqwest = "0.12"
[patch.crates-io]
http = { git = "https://github.com/sunadase/http.git" }
# Or use a local path:
# http = { path = "/path/to/this/http" }use http::header::{HeaderMap, HeaderName, HeaderValue};
use std::str::FromStr;
fn main() {
// Custom headers preserve case
let custom = HeaderName::from_str("X-Custom-Header").unwrap();
assert_eq!(custom.as_str(), "X-Custom-Header"); // Case preserved!
// Standard headers are normalized to lowercase
let standard = HeaderName::from_str("Content-Type").unwrap();
assert_eq!(standard.as_str(), "content-type");
// Case-insensitive equality still works
let h1 = HeaderName::from_str("X-Custom-Header").unwrap();
let h2 = HeaderName::from_str("x-custom-header").unwrap();
assert_eq!(h1, h2); // Equal despite different casing
// HeaderMap lookups are case-insensitive
let mut map = HeaderMap::new();
map.insert(h1, HeaderValue::from_static("value"));
assert!(map.get("x-custom-header").is_some()); // Found!
}The verification/ directory contains a working example project that demonstrates:
- How to use
[patch.crates-io]to substitute this fork - A Python server + Rust client test confirming case preservation works end-to-end
To run the verification:
cd verification
python3 server.py &
cargo run
# Should output: VERIFICATION SUCCESS: X-Custom-Header found# Run all tests
cargo test
# Run case preservation specific tests
cargo test --test case_preservation
cargo test --test e2e_verification-
Reprenum simplified - RemovedStandardPreservedvariant, using onlyStandardandCustom -
Customwrapper usesByteStr- Stores original bytes, implements case-insensitiveHashandPartialEq -
from_staticupdated - Case-insensitive matching for standard headers in const context -
parse_hdrupdated - Creates lowercase buffer for standard header matching while preserving original input for custom headers -
Internal Renaming - Renamed
MaybeLowertoMaybeValidatedto accurately reflect that custom headers are validated for safety but not necessarily lowercased -
Validation Tables - Introduced separate
HEADER_CHARS(case-preserving) andHEADER_CHARS_LOWER(normalizing) tables for flexible parsing
src/header/name.rs- Header name parsing and storagesrc/byte_str.rs- Case-insensitive hashing for ByteStr
- Standard headers don't preserve case -
Content-Typealways becomescontent-type - HTTP/2 and HTTP/3 compliance - These protocols require lowercase headers anyway, so case preservation only matters for HTTP/1.1
- This is a fork - You'll need to maintain updates from upstream
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
Based on hyperium/http by:
- Alex Crichton
- Carl Lerche
- Sean McArthur