Class CaseInsensitiveHeaderMap

java.lang.Object
java.util.AbstractMap<K,V>
java.util.HashMap<K,V>
java.util.LinkedHashMap<String,List<String>>
org.mockserver.templates.engine.model.CaseInsensitiveHeaderMap
All Implemented Interfaces:
Serializable, Cloneable, Map<String,List<String>>

public class CaseInsensitiveHeaderMap extends LinkedHashMap<String,List<String>>
A LinkedHashMap of HTTP header name -> values whose lookups (get, containsKey, getOrDefault) are case-insensitive on the header name, while iteration order and the original key casing are preserved exactly as inserted.

HTTP field names are case-insensitive (RFC 9110 §5.1) and MockServer's matchers treat them so, but response templates read request headers through plain Map semantics — Velocity's Uberspector maps $request.headers.host to get("host") and Mustache's fetcher calls containsKey then get. With a plain LinkedHashMap a template that asked for $request.headers.Host could therefore miss a header the client sent as host (or vice-versa), and the mismatch was protocol-dependent because HTTP/2 lowercases field names on the wire while HTTP/1.1 preserves the client's casing (issue #2575).

This map is deliberately additive, not normalising:

  • An exact-case hit is returned unchanged (fast path); only a miss falls back to a case-insensitive scan. So every casing combination resolves — Hosthost, hostHost, and the two same-case combinations that already worked — and keys are never lowercased, so $request.headers.Host keeps working when the client sent Host.
  • Extending LinkedHashMap keeps iteration (via keySet/values/ entrySet, used both by loop-over-headers templates and by Jackson serialisation) in the original insertion order with the original wire casing — unlike a TreeMap<>(String.CASE_INSENSITIVE_ORDER), which would reorder alphabetically.
  • If two differently-cased spellings of the same name arrive as separate fields (e.g. Host and host), put(java.lang.String, java.util.List<java.lang.String>) merges their values into the single first-seen entry rather than one silently overwriting the other, matching HTTP's rule that same-name fields combine. The first-seen casing is kept as the canonical key.

The case-insensitive resolution is a scan of the live entries rather than a maintained lower-case index, deliberately: HashMap routes its bulk and default methods (putAll, putIfAbsent, merge, compute*) straight at its internal storage without ever calling the overridden put(java.lang.String, java.util.List<java.lang.String>), and the keySet/entrySet views mutate it directly too, so any side index silently goes stale — and this map is public and reachable from template expressions, where Velocity's Uberspector will invoke exactly those public methods. Deriving from the entries instead cannot go stale by construction. Header counts are small (tens of entries) and the scan runs only on a miss, so the cost is immaterial.

Only the response/forward templates package uses this. It is intentionally NOT the codebase's general header type: org.mockserver.model.Headers is a Multimap<NottableString, NottableString> that serialises as a DTO array and offers no Map.get semantics, so it cannot be consumed by the template engines here.

Header name lookup only — this type is not used for cookies, query-string parameters or path parameters, whose names are case-sensitive per their respective specifications.

See Also: