Replaces the case-insensitive linear scan in
HeaderMap.{get,set,has} with a length-first + case-insensitive
byte-compare lookup against a fixed table of RFC 7230 + RFC 7231
- RFC 7232 + RFC 7233 + RFC 7234 + RFC 7235 + RFC 9110 + RFC 6265
header names.
Background
HeaderMap.get(name) walks the underlying List[String]
linearly, ASCII-lowercase comparing each entry. With ~10 headers
in a typical request, that's ~10 case-folded compares per lookup.
Most headers users actually look up are standard (Host,
Content-Type, Content-Length, Connection, User-Agent, Accept,
Cookie, Authorization, etc.). For those, a length-first dispatch
into a pre-canonicalised lowercase table is a single len(slice) == K branch + a small per-length switch + one byte compare —
faster than the loop and, more importantly, stable in latency
regardless of how many other headers the request carries.
The deliverable here is the dispatch primitive: given a byte
slice, return an integer index 0..N-1 if it matches a known
standard header (case-insensitively), else -1. Wiring into the
HeaderMap hot path is a follow-up; this module provides
the table + tests + the public StandardHeader.* index
constants for downstream code that wants to talk in indices
rather than strings (e.g. the upcoming SIMD HPACK decoder needs
a static-table-resolution side channel).
Approach
A "perfect hash" against a small fixed string set is most
practically implemented as length-bucketed dispatch +
case-insensitive memcmp. Mojo doesn't have a CHD / BDZ PHF
generator; for ~70 inputs the length-first approach is O(1) per
lookup with a tiny constant factor (single length compare → at
most 8-10 byte compares for the longest length-bucket), and it
generates instructions the branch predictor handles trivially.
Length 4 holds {Host, Date, From, ETag, Vary}. Length 5 holds
{Range, Allow}. Length 6 holds {Accept, Cookie, Server, Pragma,
Origin}. Length 7 holds {Referer, Trailer, Upgrade, Expires,
Warning, Refresh}. Etc. Most lengths have 1-5 candidates; the
pathological lengths (8 with {If-Match, If-Range, Location,
Set-Cookie/2-bytes-shy, etc.}) cap at ~6 candidates.
ASCII case folding
HTTP/1.1 header field names are case-insensitive (RFC 7230
§3.2). The lookup folds each input byte to lowercase via the
classic b | 0x20 trick gated to ASCII letters [A-Z]. The
table itself stores the canonical lowercase form; one fold +
one byte compare per character.
What this module provides
StandardHeader — namespace struct holding the canonical
index constants (StandardHeader.HOST,
StandardHeader.CONTENT_TYPE, etc.). Indices are stable
for the lifetime of a flare release; downstream code
(HeaderMap fast-path slot, SIMD HPACK decoder static table)
can take dependencies on them.
standard_header_count() -> Int — number of standard
headers in the table (currently 70).
standard_header_name(index) -> StaticString — inverse of
the lookup; returns the canonical lowercase name for a
valid index or empty StaticString for out-of-range.
lookup_standard_header_bytes(slice) -> Int — the lookup
primitive. Returns the index 0..N-1 on a hit (case-
insensitive byte-compare against the table) or -1 on a miss.
lookup_standard_header_string(name) -> Int — convenience
String-typed wrapper.
is_standard_header(slice) -> Bool — boolean shorthand
for callers that don't need the index.
The 70-name table covers (grouped by RFC):
- Connection control (RFC 7230): Host, Connection,
Keep-Alive, Transfer-Encoding, Trailer, Upgrade,
Content-Length, Content-Type, Date, Via.
- Conditionals (RFC 7232): ETag, If-Match, If-None-Match,
If-Modified-Since, If-Unmodified-Since, Last-Modified.
- Range (RFC 7233): Accept-Ranges, Range, If-Range,
Content-Range.
- Caching (RFC 7234): Cache-Control, Age, Expires, Pragma,
Warning.
- Authentication (RFC 7235): Authorization,
WWW-Authenticate, Proxy-Authenticate,
Proxy-Authorization.
- Negotiation (RFC 9110): Accept, Accept-Encoding,
Accept-Language, Accept-Charset, Allow, Content-Encoding,
Content-Language, Content-Location, Content-Disposition,
Vary, From, Referer, User-Agent, Server, Location,
Retry-After.
- Cookies (RFC 6265): Cookie, Set-Cookie.
- CORS (W3C / Fetch): Origin, Access-Control-Allow-Origin,
Access-Control-Allow-Methods, Access-Control-Allow-Headers,
Access-Control-Allow-Credentials, Access-Control-Expose-
Headers, Access-Control-Max-Age, Access-Control-Request-
Method, Access-Control-Request-Headers.
- Forwarding / Proxy (RFC 7239 + de facto): Forwarded,
X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host,
X-Real-IP.
- WebSocket / Upgrade: Sec-WebSocket-Key, Sec-WebSocket-
Version, Sec-WebSocket-Accept, Sec-WebSocket-Protocol,
Sec-WebSocket-Extensions.
- Misc widely-used: Expect, Refresh, X-Request-ID,
X-Content-Type-Options, X-Frame-Options, X-XSS-Protection,
Strict-Transport-Security, Content-Security-Policy,
Public-Key-Pins.
v0.10 wiring result -- MEASURED, NOT ADOPTED. Wiring this into
HeaderMap (a parallel List[Int] of per-entry ids so a lookup
becomes an integer compare) was a consistent regression: interleaved
30 s A/B on the full-parse baseline gave -3.2 %, -2.8 %, -6.8 % across
three paired runs. The extra list allocation per HeaderMap costs
more than the faster compare saves, because the server builds a map
per request but performs only about one lookup on it.
It would pay on a map that is looked up many times -- a middleware
stack reading a dozen headers, say. Re-measure before wiring; do not
assume.