Fix HTTP header casing bugs across HTTP/1.1 and HTTP/2
Separate case-insensitive field names from field-value rules, diagnose every proxy hop, and prevent duplicate-header parsing gaps.
An API that accepts Content-Type but rejects content-type is not enforcing HTTP semantics; it is exposing a parser, framework, proxy, or signature bug. Field names are case-insensitive across HTTP semantics. Yet that simple rule does not make the entire header line case-insensitive, and HTTP/2 adds a wire-level lowercase requirement that often reveals confused implementations.
Separate names from values
RFC 9110 defines a field name as case-insensitive. These identify the same field:
Content-Type: application/json
content-type: application/json
CONTENT-TYPE: application/json
Use conventional capitalization when it helps humans read HTTP/1.1 diagnostics, but never use casing as an application distinction. Field values follow the grammar and semantics defined for that particular field. Some tokens are case-insensitive; quoted strings, credentials, boundary parameters, opaque identifiers, and application-defined content can be case-sensitive. Normalize only when the field’s specification permits it.
HTTP/2 changes serialization, not the meaning
HTTP/2 requires field names to be converted to lowercase before encoding, and a message containing an uppercase field name is malformed. That is a protocol encoding rule, consistent with the case-insensitive semantic rule. An HTTP/1.1-to-HTTP/2 gateway can therefore change Content-Type to content-type without changing the request.
Pseudo-header fields such as :method and :path are not ordinary extension names. They have protocol-specific ordering and presence rules. Do not pass arbitrary colon-prefixed names through an application header map.
Diagnose the entire hop chain
Reproduce with a harmless endpoint and send the same request twice, changing only the field-name casing:
curl --http1.1 -sS -D - -o /dev/null \
-H 'X-Trace-Probe: case-test' https://example.test/health
curl --http1.1 -sS -D - -o /dev/null \
-H 'x-trace-probe: case-test' https://example.test/health
Use a non-sensitive marker and an approved test host. Record the protocol negotiated at each hop. Inspect the client library, CDN or load balancer, reverse proxy, WAF, service mesh, application server, framework adapter, and application. Logging raw authorization or cookie fields to solve a casing problem creates a larger incident; allow-list the probe field and redact values.
At the application boundary, use the framework’s case-insensitive header API instead of indexing a language dictionary populated from raw spelling. Unit-test mixed-case inputs. If a signing scheme constructs a canonical request, its specification—not local aesthetics—must define lowercasing, whitespace normalization, duplicate handling, and field ordering.
Duplicates are a different problem
Because names are case-insensitive, X-Mode and x-mode are not two independent fields. Different intermediaries may combine repeated field lines, retain multiples, or reject them depending on the field definition. Values such as Set-Cookie cannot be blindly comma-joined. Security controls and applications must agree on one parsing model or attackers can exploit inconsistent duplicate handling.
Reject prohibited duplicates early, preserve fields whose specification allows repetition, and test mixed-case duplicates through the real proxy chain. Do not “fix” ambiguity by keeping the first spelling in one layer and the last spelling in another.
Failure patterns and safe fixes
- Case-sensitive map: replace it with the framework’s HTTP header abstraction or canonicalize names to lowercase at one well-defined ingestion boundary.
- HTTP/2 rejection: find the component emitting uppercase names on the HTTP/2 wire and update or correct it; do not weaken protocol validation.
- Broken signature: compare the signer and verifier’s canonical-request bytes without logging secrets, then follow the authentication scheme’s exact rules.
- WAF mismatch: ensure security inspection and the origin interpret the same normalized name and duplicate set. Never create a bypass for alternate casing.
- Value corruption: stop lowercasing whole header lines. Normalize the name separately and parse each value according to its registered grammar.
- Observability split: aggregate metrics under a normalized field name while retaining safe evidence about the receiving hop and protocol.
Verification, rollout, and rollback
Create a matrix covering HTTP/1.1 and HTTP/2, conventional/lower/mixed-case names, allowed duplicates, forbidden duplicates, and at least one case-sensitive value. Assert identical application behavior where semantics are equivalent and a clean protocol error where the wire format is invalid. Include the production proxy topology in integration tests; a direct localhost success proves little.
Deploy normalization to a canary and compare request counts, authentication failures, WAF decisions, cache keys, and upstream status codes. Rollback restores the previous parser while retaining the tests and captured non-secret examples. If inconsistent parsing created an authorization or cache incident, preserve logs, invalidate affected cache entries or credentials as appropriate, and review neighboring headers for the same defect.
Attribution and current verification
This guide was prompted by Svish’s Stack Overflow question and Ignacio Vazquez-Abrams’s accepted answer, used under CC BY-SA. The accepted answer cited now-obsolete RFC 2616; the rule was independently verified and updated against current RFC 9110 field-name semantics and the RFC 9113 HTTP/2 field-validity requirements.
Primary source: Review the official reference ↗