The conformance / interop test drivers and the runnable demo host for the
HTTP/1.0 + HTTP/1.1 stack that lives in the Vanaheimr Hermod library — a
from-scratch implementation built directly on TcpClient/SslStream (no
Kestrel, no System.Net.Http stack, everything hand-rolled). Hermod is pulled in
here as a git submodule under libs/.
This is the HTTP/1.1 sibling of
HTTP2ConformanceTests and
HTTP3ConformanceTests, and
it follows the same idea: the protocol-level unit and integration tests live
next to the stack in HermodTests/, while this repository adds the runnable
host, the raw-wire harnesses that no high-level client can produce, and the
drivers for third-party conformance suites (curl, Autobahn, request-smuggling
differentials, real reverse proxies, real browsers).
⚠️ Reference implementation — built for learning and for owning the wire protocol end-to-end, not as a drop-in production web server.
The demo host and the raw-wire harnesses are in place, CI gates every push
on Windows and Debian 13, and the first third-party conformance suite — Autobahn —
runs nightly and gates on its result (see PLAN.md for the rest).
tests/run-tests.sh # 313 checks, ~135 stests/ raw-wire harnesses |
211/211 — syntax, framing, connection management, RFC 9110 semantics, SSE, smuggling/hardening |
tests/curl-matrix.sh |
78/78 (79 on the CI Debian leg — see the conditional checks) — the first checks here made by a client nobody in this repository wrote |
| both, over cleartext and TLS | 313 cleartext and 314 over TLS, the extra one being the ALPN check that only a TLS handshake can carry — and 487/487 with --wsl, which adds a second curl build, the foreign peers and the smuggling differential |
Demo/ host |
:8080 cleartext, :8443 TLS, :8081 WebSocket — 19 routes |
tests/autobahn.sh (nightly, gated) |
481/517 Autobahn cases — the canonical RFC 6455 suite against :8081; the 36 open ones are server_max_window_bits=9, which RFC 7692 says to decline |
tests/interop.sh |
58/58 + 3 skips — Go, Java, Node, Python and wget against our server, and our client against Go's and Node's servers. The client direction had no independent witness before this |
tests/h1bench |
not a gate — throughput, latency percentiles and allocation, with Kestrel as a control on the same loopback |
tests/h1fuzz |
~1–2 M mutated inputs per target per minute against three parsers. Fixed seed in the gate, exploring nightly. No known findings — the one it produced on its first run, H-28, was fixed on 2026-10-03 and its line deleted, which is how this harness verifies a fix |
tests/h1desync |
24/24 — 38 ambiguously framed messages; the 24 RFC 9112 states a rule for are asserted, the 14 it leaves open are observed |
tests/smuggle.sh |
38/38 over Hermod, Go net/http and Node node:http. 28 rows agree; the 10 that do not are written up and pinned in tests/smuggle-known.txt |
tests/smuggler.sh |
134/134 Transfer-Encoding obfuscations from smuggler, nothing found; h2csmuggler finds no h2c surface, which is now a pinned regression |
tests/http-garden/ |
Hermod as a target in the HTTP Garden, which compares parse trees across 45 implementations. Contract-verified on every run; the full sweep is compiler-hours and is run by hand |
tests/proxy.sh |
32/32 — the demo behind nginx, HAProxy, Caddy, Apache and Envoy. 63 differences from the direct run, each recorded by name; no chain poisons a connection, though the detector is calibrated-unfired |
tests/browser.sh |
27/27 — Chromium, Firefox and WebKit through Playwright. The three failures are one finding, H-10, on each engine |
On top of that, the coverage inside Hermod itself:
| Where | Measured by | Count | What |
|---|---|---|---|
| everything HTTP/1.x | FullyQualifiedName~Hermod.Tests.HTTP. |
980 | the two rows below plus URL/query/hostname/method models, the HTTPAPI layer and the HTTPTestServer |
| ↳ the protocol regression selection | the eleven-file filter printed in HTTP1/README.md |
379 | client/server end-to-end, framing regressions, HTTP/1.0 behaviour, pipelining, chunked + trailers, limits/timeouts, status codes, content codings in both roles, .NET interop. Unchanged by the five findings of 2026-09-26, because their four fixtures are not in the filter — adding them made it 431 when measured against 5ab74d7f. Whether they belong is a one-line change to that filter upstream, and it has not been made. It moved from 374 to 377 with H-10, whose three socket tests went into a file the filter does name, and to 379 with H-28's two, which went into HTTP11AuditRegressionTests; H-27's five, H-23's six and H-11's fourteen went into new files, and so are in the row above and not in this one — which is the same gap seen from the other side, three times in one week |
| ↳ WebSockets | FullyQualifiedName~Hermod.Tests.HTTP.WebSockets |
154 | RFC 6455 framing, handshake hardening, subprotocols, backpressure, reconnect, permessage-deflate |
| what CI actually gates on | …Tests.HTTP. + …Tests.HTTPS. |
981 | the row above plus Tests.HTTPS., which is one single test — that one test is the whole difference between 980 and 981, and it is why this table and a CI log used to disagree by one for no visible reason. What the filter does not reach is Tests.TCP and Tests.Warden, the layer AHTTPServer is built on |
Autobahn vs. the server (demo host :8081) |
tests/autobahn.sh |
481 / 517 + 36 declined | RFC 6455 + RFC 7692, nightly and gated on a floor — see tests/TestingAgainst_Autobahn.md |
Autobahn vs. the client (fuzzingserver) |
tests/autobahn-client.sh |
445 / 517 + 72 declined | our WebSocketClient driven through the suite, nightly and gated — 0 hard failures |
Every count above is a run count, re-measured on 2026-10-04 against Hermod
04dca36d and Styx c530de16 by running the filter in its own row — which is now printed, because
every time a figure here drifted it was a figure whose filter nobody could check.
--list-tests and dotnet test disagree on this checkout, and by a stable
amount: three, whatever the totals are — re-measured 2026-09-24 at 564 listed
against 561 run for Tests.HTTP., and 332 against 329 for the regression
selection. Nothing is skipped in either run, so the listing counts
three entries that never become a test case — and the same three both times,
since they sit inside the regression files. Tests.HTTP.WebSockets agrees at 49.
A number that is not the number the gate produces is not the number to publish.
Why the regression selection moved from 299 to 329, since a reader is owed
more than "it went up": five of those thirty are not new tests. The filter
itself gained three files. On 2026-09-23, HTTPStatusCodeTests (7 tests, all
new) and ContentEncodingHeaderTests (13, of which 8 are new and 5 existed all
along but had never been in the selection); on 2026-09-24,
HTTPDigestAuthenticationTests (10, all new). So 25 tests were written and 5
were merely included. Run the five-file filter this figure used to name and it
still answers 299, exactly as before: the selection grew, the older tests did
not multiply.
That filter lives in libs/Hermod/Hermod/HTTP1/README.md,
which recorded 295 on 2026-07-18 and now records 329. Those two are not
comparable either: the 295 was a list count.
- .NET 10 SDK (
net10.0target) bashfor the runner scripts — on Windows, the Git Bash that ships with Git for Windows. There is no PowerShell runner in this repository and there never was; see A11 inPLAN.mdfor why the two sibling repositories removed theirscurl— any recent build; see the note on the two builds below- WSL/Debian with Docker for the container-based suites (Autobahn, the proxy matrix, http-garden). Not Docker Desktop
- optional: Python 3 for the smuggling drivers, Node for the browser drivers
The HTTP/1.x stack lives in the Vanaheimr Hermod library, which — together
with Styx — is pulled in as a git submodule under libs/. Clone with
submodules, otherwise libs/Hermod and libs/Styx are empty and nothing
builds:
git clone --recurse-submodules https://github.com/Vanaheimr/HTTP1ConformanceTests.gitAlready cloned without --recurse-submodules?
git submodule update --init --recursivedotnet build HTTP1.slnxThe Hermod-side regression suites (these work today):
dotnet test libs/Hermod/HermodTests/HermodTests.csproj --filter "FullyQualifiedName~HTTPClientTests|FullyQualifiedName~HTTPServerSocketRegressionTests|FullyQualifiedName~HTTPClientProtocolRegressionTests|FullyQualifiedName~HTTP11AuditRegressionTests|FullyQualifiedName~HTTPServerListenerMatrixTests"# HTTP/1.1 WebSockets only — a bare "~WebSocket" filter also picks up the
# HTTP/2 (RFC 8441) and HTTP/3 (RFC 9220) WebSocket tests:
dotnet test libs/Hermod/HermodTests/HermodTests.csproj --filter "FullyQualifiedName~Tests.HTTP.WebSockets"The stack's own reference — API, limits, exclusions — lives next to the code:
libs/Hermod/Hermod/HTTP1/README.md— HTTP/1.0 + HTTP/1.1libs/Hermod/Hermod/HTTP1/WebSocket/README.md— RFC 6455 + RFC 7692
Roughly 55 000 lines under Hermod/HTTP1/ (100 files) plus the shared HTTP
model under Hermod/HTTP/ (methods, status codes, URL/path/query, content
types, cookies, authentication, the HTTP API layer).
| Subsystem | Path | State |
|---|---|---|
| Message model (PDU, builders, header fields) | HTTP1/AHTTPPDU.cs, HTTP1/{Request,Response}/ |
complete, typed header-field model |
| Server | HTTP1/Server/ |
AHTTPServer, HTTPServer, HTTPTestServer, HTTPConnection, URL routing, pipelines |
| Client | HTTP1/Client/ |
HTTPClient, HTTPSClient, HTTPClientPool, logging, SOAP |
| Chunked transfer coding | HTTP1/Server/ChunkedTransferEncoding/ |
send + receive, extensions, trailers, metadata limits |
| Server-Sent Events | HTTP1/ServerSentEvents/ |
server + client, history/replay, Last-Event-ID, retry |
| WebSocket | HTTP1/WebSocket/ |
server + client, RFC 6455 + RFC 7692, Autobahn-verified |
| Auth values | HTTP/Authentication/ |
Basic, Bearer, Token, TOTP, WWW-Authenticate builder |
Hermod deliberately separates wire protocol from application semantics. The matrix below uses these markers:
| Meaning | |
|---|---|
| ✅ | implemented and covered by automated tests in HermodTests/ |
| 🟢 | implemented, tested only indirectly / manually |
| 🟡 | model only — the header/method/status is typed, parsed and serialized, but Hermod applies no automatic policy; the handler decides |
| 🔵 | by design the handler's job — Hermod is an origin server, not a cache/proxy/framework |
| ❌ | not implemented (and would be a real gap) |
| ⬜ | out of scope for an HTTP/1.x origin client/server |
The distinction matters: "Hermod has a Range header field" is not the same
claim as "Hermod serves 206 Partial Content". The former is 🟡, the latter is
🔵 — the handler builds the partial representation.
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 9110 | HTTP Semantics (STD 97) | ✅ core: methods, status codes, Host, connection handling, Expect: 100-continue, bodyless responses, representation metadata, extension fields. Resource semantics 🔵 |
| RFC 9111 | HTTP Caching (STD 98) | ❌ no cache. Cache-Control/Age/Expires/Vary are 🟡. (the HTTP/2 stack has HTTPCache.cs — HTTP/1 does not) |
| RFC 9112 | HTTP/1.1 message syntax & routing (STD 99) | ✅ start lines, header parsing, framing, persistence, pipelining, chunked, trailers, malformed-message rejection |
| RFC 9113 | HTTP/2 | ⬜ separate stack — see HTTP2ConformanceTests. Note: Upgrade: h2c was removed by 9113 and is correctly absent |
| RFC 9114 | HTTP/3 | ⬜ separate stack — see HTTP3ConformanceTests |
| RFC 9205 | Building Protocols with HTTP (BCP 56bis) | 🔵 guidance for applications on top |
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 1945 | HTTP/1.0 | ✅ requests/responses, Content-Length, close-delimited bodies, default close, negotiated keep-alive, chunked rejection, HTTP/1.0 status line |
| RFC 2616 | HTTP/1.1 (obsolete) | 🟢 superseded by 9110/9112; still the citation in ~70 legacy source comments → cleanup ToDo |
| RFC 7230–7235 | HTTP/1.1 series (obsolete) | 🟢 superseded by 9110–9112 |
| RFC 2145 | Use and interpretation of HTTP version numbers | ✅ non-canonical versions (HTTP/1, HTTP/01.1, wrong case) are rejected |
| "HTTP/1.0 Keep-Alive" (never standardized) | Connection: Keep-Alive + Keep-Alive field |
✅ honoured only when negotiated in both directions; close wins |
| Spec / section | Topic | Hermod HTTP/1.x |
|---|---|---|
| RFC 9112 §2–3 | Message format, request line, request-target forms | ✅ origin-form ✅, asterisk-form (OPTIONS *) ✅, absolute-form ❌ by design (not a proxy), authority-form (CONNECT) → 501 |
| RFC 9110 §5 | Field syntax, obs-fold, whitespace before :, control chars |
✅ all rejected |
| RFC 9112 §6 | Message body length, the 7-step algorithm | ✅ incl. duplicate/conflicting Content-Length, CL+TE conflict, overflow/sign/non-decimal |
| RFC 9112 §7 | Transfer codings | ✅ chunked (both directions, both roles), chunk extensions, trailers, forbidden-trailer list. gzip/deflate/compress transfer codings ❌ |
| RFC 9112 §9 | Persistence, pipelining, Connection |
✅ persistent by default, pipelining in wire order, chunked request delimited before the next, invalid leading request closes the connection |
| RFC 9112 §11.2 | Message smuggling / response splitting | ✅ strict rejection of ambiguous framing, and verified differentially since 2026-09-27: tests/h1desync 24/24 against the requirements RFC 9112 states, three implementations compared in tests/smuggle.sh, two third-party probe suites, and Hermod added to the HTTP Garden. One finding, H-29 — see docs/TestingAgainst_Smuggling.md |
| RFC 9110 §7.6.1 | Connection-specific (hop-by-hop) fields | 🟡 Connection, Trailer, Upgrade, Via typed |
| RFC 7239 | Forwarded HTTP extension |
❌ — only X-Forwarded-For is typed. Marked //ToDo in HTTPRequest.cs:1125 |
| RFC 9440 | Client-Cert / Client-Cert-Chain |
❌ (mTLS itself is supported at the TLS layer) |
| RFC 8586 | CDN-Loop |
⬜ |
| RFC 9209 | Proxy-Status |
⬜ |
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 3986 | URI Generic Syntax (STD 66) | ✅ URL, HTTPPath, QueryString, URIScheme, URLHost; fragments, bad percent-escapes, encoded separators, dot segments, repeated slashes and double-encoding all rejected |
| RFC 6874 | IPv6 zone identifiers in URIs | ❌ |
| RFC 5234 / RFC 7405 | ABNF (STD 68) | ✅ used as the parsing reference |
| RFC 3629 | UTF-8 (STD 63) | ✅ incl. the incremental validator in the WebSocket path |
| RFC 9110 §5.6.7 + RFC 1123 | HTTP-date (IMF-fixdate) | 🟢 IMF-fixdate produced/parsed; obsolete RFC 850 and asctime forms ❌ |
| RFC 9651 | Structured Field Values (obsoletes 8941) | ❌ no structured-field parser/serializer |
| RFC 8187 | Charset/language in field parameters (filename*) |
❌ |
| RFC 6266 | Content-Disposition in HTTP |
🟡 typed field, no parameter-encoding logic |
| RFC 8288 | Web Linking (Link) |
❌ |
| RFC 6570 | URI Templates | ⬜ routing uses its own path-parameter syntax |
| Spec | Methods | Hermod HTTP/1.x |
|---|---|---|
| RFC 9110 §9 | GET HEAD POST PUT DELETE OPTIONS TRACE CONNECT |
✅ routed, incl. server-wide OPTIONS *, resource OPTIONS, 405 + Allow. TRACE 🟡 (token + client only, no server echo). CONNECT → 501 ⬜ |
| RFC 5789 | PATCH, Accept-Patch |
🟡 method + Accept-Patch typed; patch-document semantics 🔵 |
| RFC 10008 | QUERY (safe, body-carrying read) |
✅ modeled safe+idempotent, end-to-end tested with fixed-length and chunked content, trailers, chunk extensions. Accept-Query 🟡, negotiation/caching 🔵 |
| RFC 4918 | WebDAV — COPY LOCK MKCOL MOVE PROPFIND PROPPATCH UNLOCK |
🟡 method tokens + DAV/Depth/Destination/If/Lock-Token/Overwrite/Timeout fields typed; no WebDAV resource implementation |
| RFC 3253 / 3744 / 5323 / 5842 | WebDAV versioning/ACL/SEARCH/BIND | ⬜ |
| — | Hermod extension methods (MIRROR SEARCH EXISTS COUNT FILTER STATUS AUTH SUBSCRIBE …, ~30) |
✅ parsed, routed, with safe/idempotent metadata; HTTPMethod.Register() for application methods |
| Spec | Codes | Hermod |
|---|---|---|
| RFC 9110 §15 | 1xx/2xx/3xx/4xx/5xx core set | ✅ present and used |
| RFC 8297 | 103 Early Hints |
❌ not defined |
| RFC 7538 → 9110 §15.4.9 | 308 Permanent Redirect |
❌ not defined |
| RFC 9110 §15.5.20 | 421 Misdirected Request |
❌ not defined |
| RFC 8470 | 425 Too Early |
❌ the slot 425 is present but named NoCode (a stale RFC 2518 draft name) — bug |
| RFC 7725 | 451 Unavailable For Legal Reasons |
❌ not defined |
| RFC 6585 | 428 429 431 511 |
🟢 428/429/431 ✅ (with a token-bucket rate limiter), 511 ❌ |
| RFC 4918 / RFC 5842 | 207 422 423 424 507 / 208 508 |
🟢 207/422/423/424/507 ✅; 208/508 ❌ |
| RFC 3229 | 226 IM Used (delta encoding) |
⬜ |
| RFC 2295 | 506 Variant Also Negotiates |
🟡 code present, TCN not implemented |
| RFC 2324 | 418 I'm a teapot |
✅ obviously |
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 9110 §11 | Authentication framework, WWW-Authenticate, Authorization, Proxy-* |
✅ challenge/credential plumbing + WWWAuthenticate builder; policy 🔵 |
| RFC 7617 | Basic | ✅ typed parse/serialize + end-to-end server tests (challenge, malformed, invalid, forbidden) |
| RFC 6750 | OAuth 2.0 Bearer | 🟡 typed parse/serialize; validation 🔵 |
| RFC 7616 | Digest (SHA-256) | ❌ HTTPDigestAuthentication is not RFC 7616 — it is a custom Digest base64(user):base64(secret) scheme with no realm/nonce/qop/nc/cnonce/response. Misleading name → rename or implement |
| RFC 2617 | Basic + Digest (obsolete) | ⬜ superseded |
| RFC 9421 | HTTP Message Signatures | ❌ (Argus has its own non-RFC signature verification) |
| RFC 9530 | Digest Fields (Content-Digest, Repr-Digest) |
❌ — only the obsolete Content-MD5 (RFC 1864) is typed |
| RFC 8188 | Encrypted Content-Encoding (aes128gcm) |
❌ |
| — | Hermod extensions: Token, TOTP, API-Key |
✅ non-standard, documented as such |
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 6265 | HTTP State Management | 🟢 HTTPCookie/HTTPCookies with Path, Secure, HttpOnly, SameSite, Expires; Cookie/Set-Cookie typed. Attribute-parsing edge cases untested |
| 6265bis (draft) | __Secure-/__Host- prefixes, SameSite defaults, size limits |
❌ |
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 9110 §8.8 + §13 | Validators (ETag, Last-Modified), preconditions (If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since, If-Range) |
🟡 all fields typed and exposed; no automatic 304/412 evaluation — 🔵 handler's job |
| RFC 9110 §14 | Range requests, 206, Accept-Ranges, Content-Range, multipart/byteranges |
🟡 fields typed; no automatic 206 generation — 🔵 |
| RFC 8673 | Range requests for unknown-length content | ⬜ |
| RFC 9111 | Caching | ❌ no cache, client- or server-side |
| RFC 5861 | stale-while-revalidate, stale-if-error |
❌ |
| RFC 8246 | Cache-Control: immutable |
❌ |
| RFC 9211 / 9213 | Cache-Status / CDN-Cache-Control |
⬜ |
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 9110 §8.4 | Content codings | ❌ no codec — Content-Encoding/Accept-Encoding are 🟡 header models only. Neither the server nor the client compresses or decompresses. (the HTTP/2 stack has HTTP2/Core/HTTPContentCoding.cs with br/gzip/deflate — HTTP/1 has nothing) |
| RFC 1950 / 1951 / 1952 | ZLIB / DEFLATE / GZIP | ❌ for HTTP bodies (DEFLATE is used by WebSocket permessage-deflate) |
| RFC 7932 | Brotli (br) |
❌ |
| RFC 8878 | Zstandard (zstd) |
❌ |
| RFC 9110 §12 | Proactive negotiation — Accept, Accept-Charset, Accept-Language, Accept-Encoding, Vary |
🟢 AcceptTypes.BestMatchingContentType() for media types; language/charset/encoding negotiation and Vary bookkeeping 🔵 |
| RFC 2295 / 2296 | Transparent content negotiation | ⬜ |
| RFC 6838 / 6839 | Media types, +json/+xml suffixes |
🟢 HTTPContentType |
| RFC 7578 | multipart/form-data |
🟡 content type modeled, no multipart parser |
| RFC 9457 | Problem Details (application/problem+json) |
❌ |
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 9110 §7.8 + RFC 9112 §9.6 | Upgrade, 101 Switching Protocols |
🟢 the WebSocket server performs the upgrade; the general HTTP server has no Upgrade dispatch — the two are separate listeners |
| RFC 6455 §4 | WebSocket handshake | ✅ see below |
| RFC 2817 | Upgrading to TLS within HTTP/1.1 | ⬜ |
| RFC 9113 (removes RFC 7540 §3.2) | Upgrade: h2c |
⬜ correctly not implemented — h2c upgrade was removed from the standard, and its absence is a smuggling defence (h2csmuggler → plan item) |
| RFC 7838 | Alt-Svc — advertising h2/h3 |
❌ — would be the natural bridge to the h2/h3 stacks |
| Spec | Title | Hermod |
|---|---|---|
| RFC 6455 | The WebSocket Protocol | ✅ server + client. Framing, masking, fragmentation state machine, control-frame rules, strict UTF-8 (§8.1) with incremental validation, close handshake + status-code validation, handshake validation both directions (400/426), CSPRNG nonces and masking keys, subprotocol negotiation. Autobahn: 296 + 242 cases, 0 failed |
| RFC 7692 | permessage-deflate |
✅ negotiated, sync-flush, RSV1 only on the first frame, decompression-bomb guard. Autobahn 12/13: 126 + 126, 0 failed. Limitation: window bits < 15 unsupported (DeflateStream), no_context_takeover always negotiated |
| RFC 8307 | Well-Known URIs for WebSocket | ⬜ |
| RFC 8441 / RFC 9220 | WebSocket over HTTP/2 / HTTP/3 | ⬜ here — implemented in the HTTP/2 stack (HTTP2/WebSocket/) |
| RFC 6454 | The Web Origin Concept | ✅ AllowedOrigins allow-list → 403 (CSWSH protection) |
| — | Hardening: handshake timeout, max handshake size, per-IP connection limits, message-size limits, backpressure, heartbeat/zombie detection, reconnect with backoff+jitter | ✅ |
| Spec | Title | Hermod |
|---|---|---|
| WHATWG HTML — Server-Sent Events | text/event-stream |
✅ server + client. id/event/data/retry, multi-line data, bounded history, Last-Event-ID replay, per-request filtering, comment heartbeats, cancellation and disconnect cleanup. Not an RFC |
| RFC 6202 | Long polling / streaming best practices | 🟢 informational; the SSE implementation follows it |
| Spec | Title | Hermod HTTP/1.x |
|---|---|---|
| RFC 8446 | TLS 1.3 | ✅ via SslStream |
| RFC 7301 | ALPN | 🟢 ApplicationProtocols is settable on the client; the server does not negotiate http/1.1 explicitly → plan item |
| RFC 9525 | Service identity in TLS (obsoletes 6125) | 🟢 delegated to SslStream + configurable validators |
| RFC 2818 | HTTP over TLS | 🟢 superseded by RFC 9110 §4.2.2 |
| RFC 6797 | HSTS | ❌ no Strict-Transport-Security |
| RFC 9110 §7.2 | Host vs. TLS SNI consistency |
🟢 Host validated; SNI/Host cross-check not enforced |
| Spec | Topic | Hermod |
|---|---|---|
| WHATWG Fetch | CORS | 🟡 Access-Control-* response fields typed and settable; no automatic preflight handling |
| W3C CSP | Content-Security-Policy |
❌ |
| — | X-Frame-Options |
🟡 typed |
| WHATWG HTML | EventSource, WebSocket APIs |
🟢 the browser side of SSE + WS — verified manually, not in CI → plan item |
HTTP/1.1 has no single canonical conformance suite the way HTTP/2 has h2spec. Coverage therefore has to be assembled from several independent third-party tools — which is arguably better, because each of them is a real-world consumer with its own strictness.
| Tool | Kind | What it proves | Availability here |
|---|---|---|---|
| curl | client | the reference HTTP client: --http1.0/--http1.1, chunked, Expect: 100-continue, keep-alive, ranges, cookies, Basic/Digest auth, HEAD, --raw, -v wire traces |
✅ two builds, deliberately: Windows 8.21 without HTTP/2 (cannot accidentally upgrade) and Debian 8.14 with nghttp2/nghttp3 (proves --http1.1 and ALPN are honoured) |
| Autobahn TestSuite | WebSocket | the canonical RFC 6455 + RFC 7692 suite, both fuzzingclient (tests our server) and fuzzingserver (tests our client) |
✅ via WSL/Debian Docker |
| http-garden | differential fuzzer | request smuggling / parser differentials against 45 real servers and proxies — the state of the art for RFC 9112 §11.2 | ✅ Hermod added as a target, tests/http-garden.sh |
| smuggler.py | scanner | CL.TE / TE.CL / TE.TE desync probes | ✅ tests/smuggler.sh, nightly — 134/134, nothing found |
| h2csmuggler | scanner | Upgrade: h2c smuggling — must find nothing, since h2c upgrade is absent |
✅ tests/smuggler.sh, and the negative is pinned |
| nginx / HAProxy / Envoy / Apache httpd / Caddy / Traefik | intermediaries | reverse-proxying Hermod. Proxies are the strictest HTTP/1.1 consumers in existence — framing bugs surface here first. Also as upstreams for Hermod's client | ✅ via WSL/Debian Docker |
Go net/http, Python httpx/aiohttp, Java HttpClient/OkHttp, Rust hyper |
reference peers | independent, strict, non-.NET implementations on both sides of the wire | 🟡 per-runtime setup, in WSL |
.NET HttpClient / Kestrel / Minimal API |
reference peer | already used inside HermodTests/HTTP/dotNET/ |
✅ |
h2load (--h1), wrk, bombardier, oha, k6 |
load | keep-alive reuse and pipelining under concurrency; connection-lifecycle leaks | 🟡 apt/Docker in WSL |
| Playwright (Chromium/Firefox/WebKit) | browsers | EventSource, WebSocket, CORS preflight, chunked rendering, keep-alive — the acceptance test that matters most in practice |
✅ Node present |
websocat, ws, Python websockets |
WebSocket clients | independent WS peers beyond Autobahn | 🟡 |
| testssl.sh | TLS | the HTTPS listener's TLS posture (adjacent to, not part of, HTTP conformance) | 🟡 |
openssl s_client, nc, tshark |
raw wire | hand-crafted byte sequences; tshark's dissector as an independent framing validator |
✅ |
| cache-tests.fyi | caching | only relevant if an RFC 9111 cache is ever added (H-5) | ⬜ |
Container host: everything above that needs Docker runs in WSL/Debian
(Docker 26.1.5, already installed) rather than Docker Desktop. WSL has no
systemd, so the daemon needs sudo service docker start after a reboot — the
runner scripts do that themselves. The demo host runs on Windows, so from
inside a container it is reachable via the host IP, not localhost.
Not applicable: h2spec, nghttp2's nghttp/nghttpd, the QUIC interop
runner — all HTTP/2- or HTTP/3-specific.
Planned — mirrors the HTTP/2 repository, see PLAN.md for the
sequencing:
HTTP1ConformanceTests/ solution HTTP1.slnx (at the repo root)
├── libs/
│ ├── Hermod/ ← git submodule (Vanaheimr Hermod)
│ │ ├── Hermod/HTTP1/ the hand-rolled HTTP/1.x stack
│ │ ├── Hermod/HTTP/ shared HTTP model (methods, status, URL, …)
│ │ └── HermodTests/ the existing NUnit suites (Tests.HTTP.*)
│ └── Styx/ ← git submodule (Vanaheimr Styx)
├── Demo/ demo host — http :8080, https :8443, ws :8081
├── tests/
│ ├── run-tests.sh the gate: build → start demo → all harnesses
│ ├── H1Core/ shared raw socket, checks, target parsing
│ ├── h1syntax/ request line, targets, versions, field syntax
│ ├── h1framing/ body length, transfer codings, chunks, trailers
│ ├── h1conn/ persistence, pipelining, half-close
│ ├── h1semantics/ methods, conditionals, ranges, negotiation, auth
│ ├── h1sse/ Server-Sent Events
│ ├── h1attack/ smuggling, slowloris, floods, limits
│ └── h1raw/ diagnostic wire dumper (not in the gate)
└── docs/BUILD_LOG.md chronological build history
Apache License 2.0 — © 2010-2026 GraphDefined GmbH. The full text is in
LICENSE. The Hermod and Styx submodules are likewise Apache-2.0.