# HTTP `Headers` Design Concerns — Triage List Context: input for the `roc-lang/http` redesign (the "Standardized HTTP interface for Roc" discussion). Each item: what the issue is, why it matters for Roc specifically, and a proposed direction. Sourced from RFCs, issue trackers, CVE databases, and community forum threads. --- ### 1. Header name casing: spec says insensitive, reality disagrees — and HTTP/2 forces lowercase **Issue:** RFC 9110 says header field names are case-insensitive, but real servers (legacy SMSC/telecom gateways, AWS API Gateway, S3, IoT devices) require *exact* casing like `SenderID` or `Content-Length`. Meanwhile RFC 9113 §8.2 mandates lowercase on the wire for HTTP/2/3 and treats uppercase names as malformed. **Why it matters:** Every mainstream library that normalizes case at construction time (Go's `CanonicalMIMEHeaderKey`, Rust's `HeaderName` lowercasing) destroys the original casing *irreversibly, by design*. Users then can't integrate with non-compliant servers without bypassing the library entirely (raw sockets, local proxies). This is one of the most-reported HTTP client pain points across ecosystems. **Direction:** `Headers` as an opaque type backed by an ordered list that preserves exact caller/wire casing. Case-insensitive matching (`get`, `get_all`, `get_exact`) is a function over that list, not part of storage. HTTP/2 lowercasing happens only at the wire-encoding boundary in the platform — never mutates the `Headers` value. **Links:** - RFC 9113 §8.2 (lowercase mandate): https://www.rfc-editor.org/rfc/rfc9113.html - Go issue — control header casing: https://github.com/golang/go/issues/5022 - Rust `HeaderName` (lowercase-by-design): https://docs.rs/http/latest/http/header/struct.HeaderName.html - hyper — headers lowercased, no opt-out: https://github.com/hyperium/hyper/issues/1492 - hyper — `HeaderCaseMap` side-channel retrofit: https://github.com/hyperium/hyper/issues/2873 - Real-world SMSC casing problem (Rust forum): https://users.rust-lang.org/t/solved-invalid-headers-with-reqwest-now-looking-to-preserve-header-casing/134879 - Same issue, follow-up thread: https://users.rust-lang.org/t/reqwest-clientbuilder-preserve-header-casing/137922 - linkerd2 — mesh lowercases headers, breaks case-sensitive backends: https://github.com/linkerd/linkerd2/issues/3964 - Envoy header-casing docs (preserve_case formatter): https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/header_casing - AWS API Gateway case-sensitivity complaint: https://github.com/aws/aws-lambda-go/issues/117 --- ### 2. Duplicate / multi-value headers — `Set-Cookie` can't be comma-joined **Issue:** RFC 9110 §5.3 allows merging same-name header fields with commas, but `Set-Cookie` is an explicit, documented exception — its `Expires` attribute date format contains commas, so comma-joining corrupts it. `Cookie` (request) and `Set-Cookie` (response) also have different shapes (single semicolon-separated header vs. multiple independent headers). **Why it matters:** Several mainstream libraries (Python `requests`, Node's `fetch`/`Headers`) have shipped real bugs where multiple `Set-Cookie` values got merged or dropped, silently breaking sessions. **Direction:** Store headers as an ordered list of `(name, value)` pairs that never merges duplicates. `get_all` (ordered, all values for a name) is the primary multi-value accessor; no auto-comma-joining default is offered anywhere in the API. **Links:** - RFC 9110 §5.3 (field combination rules): https://www.rfc-editor.org/rfc/rfc9110.html - Python requests — Set-Cookie comma-join bug: https://github.com/psf/requests/issues/3957 - Next.js — `get('set-cookie')` broken for multiple cookies: https://github.com/vercel/next.js/issues/54033 - better-auth — cookies silently dropped: https://github.com/better-auth/better-auth/issues/9705 - SvelteKit — can't return multiple Set-Cookie: https://github.com/sveltejs/kit/issues/3460 - msw — multiple Set-Cookie combined into one: https://github.com/mswjs/msw/issues/640 - restify — same issue: https://github.com/restify/node-restify/issues/779 - Envoy — feature request for separate Set-Cookie lines: https://github.com/envoyproxy/envoy/issues/7488 --- ### 3. Duplicate `Content-Length` / `Transfer-Encoding` → request smuggling **Issue:** If `Content-Length` and `Transfer-Encoding` are both present (or either is duplicated), front-end and back-end servers can disagree about where a message ends — the basis of CL.TE/TE.CL/TE.TE "desync" attacks. **Why it matters:** This is a major, ongoing, real-world security vulnerability class (e.g., CVE-2021-21295 in Netty via Zuul, AWS ALB's H2.TE issue). A types layer that silently picks "one" value for a duplicated framing header hides the very signal needed to detect an attack. **Direction:** `Headers` must faithfully expose duplicates rather than silently resolving them. State explicitly in the RFC that framing-header ambiguity (duplicate/conflicting `Content-Length`/`Transfer-Encoding`) is a hard error for the platform/server layer, not something the types package resolves on the app's behalf. **Links:** - PortSwigger — HTTP Desync Attacks research: https://portswigger.net/kb/papers/z7ow0oy8/http-desync-attacks.pdf - PortSwigger — HTTP/2 desync research: https://portswigger.net/research/http2 - gunicorn — bare-LF retained in header value (RFC 9110 violation enabling smuggling): https://github.com/benoitc/gunicorn/issues/3144 - RFC 9113 (HTTP/2 framing): https://www.rfc-editor.org/rfc/rfc9113.html --- ### 4. Header validation & CRLF/header injection **Issue:** RFC 9110 §5.5 defines the allowed byte grammar for header values (and forbids CR, LF, NUL); RFC 7230 §3.2.4 deprecates obsolete line-folding (`obs-fold`). Lenient parsers that don't enforce these rules have a long CVE history. **Why it matters:** Real CVEs across Python (CVE-2019-9740, CVE-2019-18348, CVE-2026-1502), Node's llhttp (CVE-2022-35256, CVE-2023-30589, CVE-2024-27982), and Netty (#10574) all trace back to insufficiently validated header names/values. **Direction:** `Headers` construction/insertion functions validate names against the `token`/`tchar` grammar and reject values containing CR/LF/NUL, returning a `Result` rather than silently stripping or accepting. Never parse `obs-fold`. **Links:** - RFC 9110 §5.5 (field value grammar): https://www.rfc-editor.org/rfc/rfc9110.html - RFC 7230 (obs-fold deprecation): https://datatracker.ietf.org/doc/html/rfc7230 - CVE-2019-9740 (Python urllib CRLF injection): https://theobservator.net/cve-2019-9740-python-urllib-crlf-injection-vulnerability/ - CVE-2019-18348 (Python urlopen host CRLF): https://python-security.readthedocs.io/vuln/urlopen-host-http-header-injection.html - CVE-2026-1502 (CPython proxy tunnel CRLF): https://windowsnews.ai/article/cve-2026-1502-cpython-http-proxy-tunnel-crlf-injection-on-windows-explained.419864 - urllib3 — CRLF injection tracking issue: https://github.com/urllib3/urllib3/issues/1553 - Netty — obs-fold enabling CRLF injection: https://github.com/netty/netty/issues/10574 - Node.js Feb 2020 security release (header parsing CVEs): https://nodejs.org/en/blog/vulnerability/february-2020-security-releases - CVE-2022-35256 (Node smuggling): https://www.sentinelone.com/vulnerability-database/cve-2022-35256/ - CVE-2023-30589 (Node smuggling): https://www.sentinelone.com/vulnerability-database/cve-2023-30589/ - CVE-2024-27982 (Node smuggling): https://www.sentinelone.com/vulnerability-database/cve-2024-27982/ --- ### 5. Header values aren't guaranteed UTF-8 — `Str` vs `List U8` **Issue:** RFC 9110 permits `obs-text` (raw bytes 0x80–0xFF) in header values for legacy compatibility. Roc's `Str` type assumes valid UTF-8. The current `roc-lang/http` source stores both header names and values as `Str`, with only the body as `List U8`. **Why it matters:** Storing header values as `Str` either rejects on-the-wire-legal bytes or risks constructing an invalid Roc string. This also affects round-trip fidelity — relevant both for legacy-server compatibility and for clients that need to reproduce exact byte sequences (e.g., to match a specific browser's header encoding). **Direction:** Store header values (and arguably names) as `List U8` internally, with an ergonomic `value_str : ... -> Result Str [BadUtf8]` accessor for the common UTF-8 case. **Links:** - RFC 9110 §5.5 (obs-text): https://www.rfc-editor.org/rfc/rfc9110.html - Current `roc-lang/http` Request type: https://github.com/roc-lang/http/blob/main/Request.roc --- ### 6. HPACK/QPACK header-table DoS — avoid eager `Dict` materialization **Issue:** HTTP/2's HPACK dynamic header table (RFC 9113 §6.5.2) has repeatedly been the basis of DoS attacks — the original "HPACK bomb", Go's quadratic-decode CVE-2022-41723, and the 2026 "HTTP/2 Bomb" (CVE-2026-49975) affecting nginx/Apache/Envoy/Pingora. **Why it matters:** A types-only package can't prevent this (no I/O), but a `Headers` design that forces eager materialization into a `Dict` keyed by attacker-controlled header names would *compound* the risk (hash-flooding on top of decode amplification) — and would also violate the "no eager Dict" performance priority already raised for this redesign. **Direction:** Keep `Headers` a plain ordered list with no eager hashing — this is a "do no extra harm" property that falls out of items 1–2 above. State explicitly in the RFC that header-count/size limits are a platform/transport responsibility, not the types package's. **Links:** - Go CVE-2022-41723 (quadratic HPACK decode): https://github.com/golang/go/issues/57855 - CVE-2026-49975 "HTTP/2 Bomb" writeup: https://www.bleepingcomputer.com/news/security/new-http-2-bomb-dos-attack-crashes-web-servers-in-under-a-minute/ - CVE-2026-49975 vendor analysis: https://www.imperva.com/blog/imperva-customers-protected-against-cve-2026-49975-http-2-bomb-dos/ - RFC 9113 §6.5.2 (dynamic table): https://www.rfc-editor.org/rfc/rfc9113.html --- ### 7. Pseudo-headers and connection-specific headers (HTTP/2/3) **Issue:** HTTP/2 represents method/path/authority/scheme/status as `:`-prefixed pseudo-headers (RFC 9113 §8.3), distinct from regular fields, and forbids `Connection`, `Keep-Alive`, `Transfer-Encoding`, `Upgrade`, `Proxy-Connection` (§8.2.2) — sending these is a protocol violation. Trailers (post-body header fields) are a third, separate category. **Why it matters:** Mixing pseudo-headers into a general `Headers` collection invites `Host` vs. `:authority` reconciliation bugs, and several real clients/proxies have shipped bugs sending forbidden connection-specific headers over HTTP/2. **Direction:** Keep pseudo-header-equivalent values (method, URI/authority, scheme, status) as separate typed fields on `Request`/`Response`, not inside the `Headers` list. Likewise give `Response` (and chunked `Request`) an explicit `trailers : Headers` field distinct from `headers`. Document the HTTP/2-forbidden header set so platforms can reject/strip before encoding. **Links:** - RFC 9113 (pseudo-headers §8.3, forbidden headers §8.2.2): https://www.rfc-editor.org/rfc/rfc9113.html - Suricata — Host vs :authority handling: https://redmine.openinfosecfoundation.org/issues/6424 - curl — connection-specific header sent over HTTP/2: https://github.com/curl/curl/issues/3832 - nghttp2 — proxy-connection causing PROTOCOL_ERROR: https://github.com/nghttp2/nghttp2/issues/887 - HTTPToolkit — translating between HTTP/1 and HTTP/2: https://httptoolkit.com/blog/translating-http-2-into-http-1/ --- ### 8. Sensitive headers on cross-origin redirects **Issue:** Clients must strip headers like `Authorization`, `Cookie`, and `Proxy-Authorization` when a redirect crosses origins. Multiple major HTTP clients have shipped CVEs for getting this wrong, in both directions (stripping too little or, in one case, wrongly *restoring* a stripped header on a later redirect). **Why it matters:** Failing to strip leaks credentials to third-party hosts via redirect — this is a recurring, serious CVE category (curl, Go, urllib3, and a Chromium-wide behavior change all address it). **Direction:** Primarily a platform/client concern (redirect-following logic lives outside the types package), but the RFC should document a canonical list of "sensitive" header names so every platform implementing redirects applies the same stripping rule rather than each reinventing — and potentially missing — it. **Links:** - curl CVE-2018-1000007 (auth leak on redirect): https://curl.se/docs/CVE-2018-1000007.html - Go CVE-2024-45336 (sensitive headers wrongly restored): https://github.com/golang/go/issues/70530 - Go CVE-2025-4673 (Proxy-Authorization persisted cross-origin): https://github.com/golang/go/issues/73816 - Go #35104 (Authorization stripping on http→https): https://github.com/golang/go/issues/35104 - urllib3 advisory (Proxy-Authorization not stripped): https://github.com/urllib3/urllib3/security/advisories/GHSA-34jh-p97f-mpxf - Chromium — remove Authorization on cross-origin redirect: https://groups.google.com/a/chromium.org/g/blink-dev/c/3Zt4UHbynYA/m/9CZ3fFdnAQAJ --- ### 9. Where this lands relative to the current redesign **Issue:** `roc-lang/http`'s current `Request` stores `headers : List (Str, Str)` directly, with no dedicated `Headers` type and no `get`/`get_all`/`get_exact` functions yet. The "Standardized HTTP interface for Roc" discussion (Roc Zulip, #ideas) is the live, unresolved design venue — Richard Feldman has stated three priorities (no edge cases discarded, no performance forcing via eager `Dict`, ergonomics), and Luke Boswell has proposed Record Builders for construction. **Why it matters:** Items 1–8 above are concrete, citable inputs to that discussion — several (1, 2, 5, 6) directly address Feldman's stated priorities and the current code's open gaps. **Direction:** Propose promoting `headers` to a dedicated opaque `Headers` type per items 1–8, as a concrete contribution to the existing Zulip thread / a follow-up RFC section. **Links:** - `roc-lang/http` repository: https://github.com/roc-lang/http - Current `Request.roc`: https://github.com/roc-lang/http/blob/main/Request.roc