{
  "what": "What real HTTP clients actually did with this server's misbehaviour, as data: one row per observation, across seven families so far — the 21 /compress flavors (six decoding clients and two non-decoding controls), the 9 /crosshost flavors (eight clients; eight boundaries and a same-origin control), the 18 /auth flavors (the same eight clients, each handed the documented fake credentials through its own mechanism), the 17 /cookies setter flavors (the same eight, each with a fresh jar where the client has one, through set, echo, delete and echo again), the 14 /sse flavors (real SSE client libraries plus a raw-wire control, each row described against a WHATWG parse of the bytes the control received) and the 14 /range flavors (real resuming download clients, each handed a 400-byte partial of the document and told to resume it, plus a fresh download, with every request recorded and the file on disk described against where RFC 9110 places the bytes a control received) and the 10 /etag flavors (real HTTP caches and conditional-request clients, each fetching the flavor three times through a fresh cache with every request recorded, the caller's answer described against what RFC 9111 implies from the control's exchanges). Every row names the client and its version, the date, what it received, and what it reported.",
  "why": "For /compress the home page has claimed these results since v0.12.0 and cited a private repository as the evidence; for /crosshost the family index carried them as prose from v0.17.0 (2026-09-10); for /auth the home page carried a three-client note from v0.9.0 (2026-08-27); for /cookies the home page carried a three-jar note from v0.6.0 (2026-08-24); for /sse the home page carried a one-client note from v0.4.0 (2026-08-23); for /range the home page carried a hand-typed curl note from v0.5.0 (2026-08-23); for /etag the home page carried a one-cache note from v0.5.0 (2026-08-23). Here are all seven as rows. It is also the only data on this service that badhttp did not write about itself.",
  "jsonl": "http://badhttp.dev/clients.jsonl",
  "rows": 959,
  "families": {
    "compress": {
      "what": "What eight HTTP implementations actually did with the twenty-one /compress flavors, one row per observation. Six of them decode content codings and are the subject; two are Python urllib profiles that never decode and serve as controls — one sending no Accept-Encoding (so its bytes are what Cloudflare's edge made of the flavor), one asking for gzip and still not decoding (so its bytes are the flavor as this server sent it).",
      "rows": 168,
      "flavors": 21,
      "observed": "2026-09-02T00:57Z",
      "badhttp_version_observed": "4b211949",
      "clients": [
        {
          "id": "curl",
          "name": "curl",
          "role": "client",
          "version": "8.7.1",
          "platform": "x86_64-apple-darwin25.0",
          "invocation": "curl --compressed",
          "accept_encoding_sent": "gzip, deflate"
        },
        {
          "id": "undici",
          "name": "Node fetch (undici)",
          "role": "client",
          "version": "node v26.8.1 / undici 8.10.0",
          "platform": "node",
          "invocation": "fetch()",
          "accept_encoding_sent": "gzip, deflate, br"
        },
        {
          "id": "urllib",
          "name": "Python urllib.request",
          "role": "control",
          "version": "Python 3.14 stdlib",
          "platform": "python",
          "invocation": "urllib.request.urlopen",
          "accept_encoding_sent": "(none)",
          "note": "Sends no Accept-Encoding and never decodes, so its bytes are what the edge made of the flavor — not a decoder under test."
        },
        {
          "id": "urllib-gzip",
          "name": "Python urllib.request (Accept-Encoding: gzip)",
          "role": "control",
          "version": "Python 3.14 stdlib",
          "platform": "python",
          "invocation": "urllib.request.urlopen",
          "accept_encoding_sent": "gzip",
          "note": "Asks for gzip and still never decodes, so its bytes are the flavor as this server sent it — the as-sent oracle, not a decoder under test."
        },
        {
          "id": "requests",
          "name": "Python requests",
          "role": "client",
          "version": "2.34.2 (urllib3 decoder)",
          "platform": "python",
          "invocation": "requests.get",
          "accept_encoding_sent": "gzip, deflate, br, zstd"
        },
        {
          "id": "httpx",
          "name": "Python httpx",
          "role": "client",
          "version": "0.28.1 (own decoders)",
          "platform": "python",
          "invocation": "httpx.get",
          "accept_encoding_sent": "gzip, deflate, br, zstd"
        },
        {
          "id": "ruby-nethttp",
          "name": "Ruby Net::HTTP",
          "role": "client",
          "version": "ruby 2.6.10",
          "platform": "ruby",
          "invocation": "Net::HTTP.get_response",
          "accept_encoding_sent": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3 (decode_content default)"
        },
        {
          "id": "go-nethttp",
          "name": "Go net/http",
          "role": "client",
          "version": "go1.27.0",
          "platform": "go",
          "invocation": "http.DefaultTransport",
          "accept_encoding_sent": "gzip"
        }
      ],
      "fields": [
        "id",
        "corpus_id",
        "family",
        "flavor",
        "url",
        "client",
        "observed",
        "badhttp_version_observed",
        "status",
        "reported_error",
        "outcome",
        "license",
        "content_encoding_seen",
        "uncompressed_flag",
        "bytes_received",
        "bytes_declared_plaintext",
        "plaintext_sha256_match",
        "first_bytes_hex",
        "oracle_header"
      ],
      "outcome_legend": {
        "plaintext": "The caller received exactly the flavor's documented plaintext (SHA-256 match), and the client reported no error.",
        "plaintext-then-error": "The caller received the whole documented plaintext AND the client reported an error — it handed over every byte and then told you something was wrong.",
        "differs-silently": "The bytes the caller received are not the documented plaintext, and the client reported nothing at all.",
        "differs-reported": "The bytes the caller received are not the documented plaintext, and the client reported an error.",
        "decode-error": "Response headers arrived; the body decode raised before a complete body reached the caller.",
        "request-failed": "The client raised before any usable response reached the caller."
      },
      "reading_this": "Read `outcome` as a description of what the caller received, never as a verdict on the client. \"differs-silently\" is the expected outcome on some flavors and the finding on others, and the difference is a property of the flavor, not of the client. On `undeclared` and `double-hidden` this server declares no coding for the client to strip, so returning the bytes exactly as sent is the expected outcome — and twelve of twelve client rows on those two flavors returned it. On `truncated` the identical outcome is the finding: five of six reported no error: four returned a partial document as though it were whole and one an empty body. GET /compress documents which is which, per flavor. No client here is scored, ranked, or called conformant: no specification says what a client must surface to its caller for most of these cases, which is exactly why the observations are worth more than an opinion.",
      "observation_counts_by_role": {
        "client:plaintext": 72,
        "client:plaintext-then-error": 8,
        "client:differs-silently": 24,
        "client:differs-reported": 5,
        "client:decode-error": 5,
        "control:plaintext": 18,
        "control:differs-silently": 22,
        "control:request-failed": 2,
        "client:request-failed": 12
      },
      "disagreement_by_flavor": {
        "not-compressed": {
          "distinct_outcomes": 4,
          "outcomes": [
            "decode-error",
            "differs-reported",
            "plaintext-then-error",
            "request-failed"
          ]
        },
        "wrong-length": {
          "distinct_outcomes": 4,
          "outcomes": [
            "decode-error",
            "plaintext",
            "plaintext-then-error",
            "request-failed"
          ]
        },
        "bad-crc": {
          "distinct_outcomes": 3,
          "outcomes": [
            "decode-error",
            "plaintext-then-error",
            "request-failed"
          ]
        },
        "corrupt": {
          "distinct_outcomes": 3,
          "outcomes": [
            "decode-error",
            "differs-reported",
            "request-failed"
          ]
        },
        "deflate-raw": {
          "distinct_outcomes": 3,
          "outcomes": [
            "differs-silently",
            "plaintext",
            "request-failed"
          ]
        },
        "multi-member": {
          "distinct_outcomes": 3,
          "outcomes": [
            "differs-reported",
            "differs-silently",
            "plaintext"
          ]
        },
        "trailing-garbage": {
          "distinct_outcomes": 3,
          "outcomes": [
            "decode-error",
            "plaintext",
            "plaintext-then-error"
          ]
        },
        "double": {
          "distinct_outcomes": 2,
          "outcomes": [
            "differs-silently",
            "plaintext"
          ]
        },
        "truncated": {
          "distinct_outcomes": 2,
          "outcomes": [
            "differs-reported",
            "differs-silently"
          ]
        },
        "unknown-coding": {
          "distinct_outcomes": 2,
          "outcomes": [
            "plaintext",
            "plaintext-then-error"
          ]
        },
        "x-gzip": {
          "distinct_outcomes": 2,
          "outcomes": [
            "differs-silently",
            "plaintext"
          ]
        },
        "bomb": {
          "distinct_outcomes": 1,
          "outcomes": [
            "plaintext"
          ]
        },
        "br": {
          "distinct_outcomes": 1,
          "outcomes": [
            "plaintext"
          ]
        },
        "deflate": {
          "distinct_outcomes": 1,
          "outcomes": [
            "plaintext"
          ]
        },
        "double-hidden": {
          "distinct_outcomes": 1,
          "outcomes": [
            "differs-silently"
          ]
        },
        "empty": {
          "distinct_outcomes": 1,
          "outcomes": [
            "plaintext"
          ]
        },
        "gzip-file": {
          "distinct_outcomes": 1,
          "outcomes": [
            "plaintext"
          ]
        },
        "ok": {
          "distinct_outcomes": 1,
          "outcomes": [
            "plaintext"
          ]
        },
        "undeclared": {
          "distinct_outcomes": 1,
          "outcomes": [
            "differs-silently"
          ]
        },
        "uppercase": {
          "distinct_outcomes": 1,
          "outcomes": [
            "plaintext"
          ]
        },
        "zstd": {
          "distinct_outcomes": 1,
          "outcomes": [
            "plaintext"
          ]
        }
      },
      "freshness": "A dated capture, not a live measurement: taken 2026-09-02T00:57Z against badhttp version 4b211949, with the client versions on each row. Client behaviour changes between releases and this table does not update itself. Re-run it from scripts/compress-witness/ in the source and the numbers move; the date on every row is how you know whether to trust it.",
      "reproduce": "Every row is one request. Fetch the row's url with the row's client at the row's accept_encoding_sent, compare what your caller receives against the response's x-badhttp-plain-sha256 header, and you have re-derived the row."
    },
    "crosshost": {
      "what": "What eight HTTP clients did with Authorization, Cookie and X-Api-Key when a redirect crossed each of the nine /crosshost boundaries, one row per observation. Each row is one chain: the client was started at the flavor url with the family's published fake test values, followed the redirect on its own, and the landing oracle reported what arrived. Eight of the nine flavors cross a boundary; same-origin is the control and crosses none. On the jar flavor the hand-set Cookie header is omitted and the client's own cookie jar (where it has one) is what is measured, against the three cookies the first hop sets.",
      "rows": 72,
      "flavors": 9,
      "observed": "2026-09-17T20:56:39Z",
      "badhttp_version_observed": "0.17.0",
      "worker_version": "0f278c14",
      "clients": [
        {
          "id": "curl",
          "name": "curl",
          "role": "client",
          "version": "8.7.1",
          "platform": "x86_64-apple-darwin26.0",
          "invocation": "curl -L --max-redirs 5",
          "cookie_jar": true,
          "jar_note": "cookie engine enabled with -b \"\" on the jar flavor"
        },
        {
          "id": "go-nethttp",
          "name": "Go net/http",
          "role": "client",
          "version": "go1.27.0",
          "platform": "darwin/arm64",
          "invocation": "http.Client{Jar: cookiejar, Timeout: 30s, CheckRedirect: counts hops, stops at 6}",
          "cookie_jar": true,
          "jar_note": "net/http/cookiejar"
        },
        {
          "id": "undici",
          "name": "Node fetch (undici)",
          "role": "client",
          "version": "node v26.8.1 / undici 8.10.0",
          "platform": "node darwin/arm64",
          "invocation": "fetch(url, {redirect: 'follow'})",
          "cookie_jar": false,
          "jar_note": "fetch() has no cookie jar; nothing stood in for one"
        },
        {
          "id": "urllib",
          "name": "Python urllib.request",
          "role": "client",
          "version": "python 3.14.7 stdlib",
          "platform": "darwin/arm64",
          "invocation": "build_opener(HTTPCookieProcessor(CookieJar()), HTTPRedirectHandler).open",
          "cookie_jar": true,
          "jar_note": "http.cookiejar.CookieJar via HTTPCookieProcessor"
        },
        {
          "id": "requests",
          "name": "Python requests",
          "role": "client",
          "version": "2.34.2 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "requests.Session().get(allow_redirects=True)",
          "cookie_jar": true,
          "jar_note": "the Session jar"
        },
        {
          "id": "httpx",
          "name": "Python httpx",
          "role": "client",
          "version": "0.28.1 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "httpx.Client(follow_redirects=True).get",
          "cookie_jar": true,
          "jar_note": "the Client jar"
        },
        {
          "id": "urllib3",
          "name": "Python urllib3",
          "role": "client",
          "version": "2.8.0 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "urllib3.PoolManager().request(redirect=True, retries=Retry(redirect=6))",
          "cookie_jar": false,
          "jar_note": "urllib3 has no cookie jar; nothing stood in for one"
        },
        {
          "id": "aiohttp",
          "name": "Python aiohttp",
          "role": "client",
          "version": "3.14.3 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "aiohttp.ClientSession().get(allow_redirects=True)",
          "cookie_jar": true,
          "jar_note": "the ClientSession jar"
        }
      ],
      "fields": [
        "id",
        "corpus_id",
        "family",
        "flavor",
        "url",
        "client",
        "observed",
        "badhttp_version_observed",
        "status",
        "reported_error",
        "outcome",
        "license",
        "hops_followed",
        "redirect_followed",
        "final_url",
        "attempts",
        "landed_on",
        "landed_port",
        "landed_scheme",
        "transport_was_encrypted",
        "sent",
        "arrived",
        "authorization_scheme_seen",
        "cookie_names_arrived",
        "cookie_other_count",
        "matches_documented_test_credential",
        "oracle"
      ],
      "outcome_legend": {
        "all-arrived": "Every header the harness sent arrived on the final hop. `sent` says what was sent: Authorization and X-Api-Key on every flavor, plus a hand-set Cookie on every flavor but jar.",
        "some-arrived": "A subset of the sent headers arrived on the final hop. The `arrived` object says which.",
        "none-arrived": "None of the sent headers arrived on the final hop.",
        "did-not-land": "The client returned a response, but not the oracle's: it stopped before the landing url, or what came back carried no x-badhttp-version (Cloudflare's rate-limit page, not this server). Recorded only if three attempts with pauses never landed.",
        "request-failed": "The client raised before any usable response reached the caller."
      },
      "reading_this": "Read `arrived` and `outcome` as a description of what reached the landing host, never as a verdict on the client. RFC 9110 §15.4 says nothing about credentials on redirects; the Fetch standard's cross-origin stripping rule governs browsers, not these clients; and several of the behaviours here are documented choices by the client's own authors — Go net/http forwards to a subdomain of the initial host by design, and Python requests keeps Authorization across http-to-https on the same host as a carve-out its source calls backwards compatibility. Whether forwarding across a given boundary is a bug depends on the boundary and on what the client documents. What a row can tell you truthfully is which headers arrived, on which host and port, over which transport, and — on the jar flavor — which of the cookies this server minted came back. Nothing sent is echoed anywhere: the oracle reports presence, a scheme name and byte length, and names only cookies under its own badhttp_ prefix — the three it mints on the jar flavor plus the harness's own badhttp_witness — and the rows carry only booleans and those names. hops_followed is null where the client exposes no count (Node fetch); redirect_followed carries the boolean it does expose, and final_url the url the chain ended on. attempts is how many tries the chain took before an oracle response arrived (a retry means the zone rate limit answered, not this server).",
      "outcome_counts": {
        "all-arrived": 22,
        "some-arrived": 50
      },
      "disagreement_by_flavor": {
        "jar": {
          "distinct_answers": 5,
          "answers": {
            "arrived: x-api-key | minted cookies: badhttp_domain": [
              "curl",
              "aiohttp"
            ],
            "arrived: x-api-key": [
              "undici",
              "urllib3"
            ],
            "arrived: x-api-key | minted cookies: __Host-badhttp_lock, badhttp_domain, badhttp_hostonly": [
              "requests",
              "httpx"
            ],
            "arrived: authorization, x-api-key | minted cookies: badhttp_domain": [
              "go-nethttp"
            ],
            "arrived: authorization, x-api-key | minted cookies: __Host-badhttp_lock, badhttp_domain, badhttp_hostonly": [
              "urllib"
            ]
          }
        },
        "scheme-upgrade": {
          "distinct_answers": 3,
          "answers": {
            "arrived: x-api-key": [
              "curl",
              "undici",
              "urllib3",
              "aiohttp"
            ],
            "arrived: authorization, cookie, x-api-key": [
              "go-nethttp",
              "urllib"
            ],
            "arrived: authorization, x-api-key": [
              "requests",
              "httpx"
            ]
          }
        },
        "boomerang": {
          "distinct_answers": 2,
          "answers": {
            "arrived: x-api-key": [
              "undici",
              "requests",
              "httpx",
              "urllib3",
              "aiohttp"
            ],
            "arrived: authorization, cookie, x-api-key": [
              "curl",
              "go-nethttp",
              "urllib"
            ]
          }
        },
        "from-subdomain": {
          "distinct_answers": 2,
          "answers": {
            "arrived: x-api-key": [
              "curl",
              "go-nethttp",
              "undici",
              "requests",
              "httpx",
              "urllib3",
              "aiohttp"
            ],
            "arrived: authorization, cookie, x-api-key": [
              "urllib"
            ]
          }
        },
        "port-change": {
          "distinct_answers": 2,
          "answers": {
            "arrived: x-api-key": [
              "curl",
              "undici",
              "requests",
              "httpx",
              "urllib3",
              "aiohttp"
            ],
            "arrived: authorization, cookie, x-api-key": [
              "go-nethttp",
              "urllib"
            ]
          }
        },
        "relative-authority": {
          "distinct_answers": 2,
          "answers": {
            "arrived: x-api-key": [
              "curl",
              "undici",
              "requests",
              "httpx",
              "urllib3",
              "aiohttp"
            ],
            "arrived: authorization, cookie, x-api-key": [
              "go-nethttp",
              "urllib"
            ]
          }
        },
        "same-origin": {
          "distinct_answers": 2,
          "answers": {
            "arrived: authorization, cookie, x-api-key": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "urllib3",
              "aiohttp"
            ],
            "arrived: authorization, x-api-key": [
              "requests",
              "httpx"
            ]
          }
        },
        "scheme-downgrade": {
          "distinct_answers": 2,
          "answers": {
            "arrived: x-api-key": [
              "curl",
              "undici",
              "requests",
              "httpx",
              "urllib3",
              "aiohttp"
            ],
            "arrived: authorization, cookie, x-api-key": [
              "go-nethttp",
              "urllib"
            ]
          }
        },
        "to-subdomain": {
          "distinct_answers": 2,
          "answers": {
            "arrived: x-api-key": [
              "curl",
              "undici",
              "requests",
              "httpx",
              "urllib3",
              "aiohttp"
            ],
            "arrived: authorization, cookie, x-api-key": [
              "go-nethttp",
              "urllib"
            ]
          }
        }
      },
      "findings": [
        "X-Api-Key arrived intact on 72 of 72 observations, including all 46 on which Authorization was stripped. The harnesses did not send Proxy-Authorization, so the oracle's proxy_authorization: false on every row is not an observation of stripping.",
        "Go net/http forwarded Authorization and the hand-set Cookie across to-subdomain, boomerang, scheme-upgrade, scheme-downgrade, port-change, relative-authority and stripped both on from-subdomain. Its shouldCopyHeaderOnRedirect() forwards to the initial host or a subdomain of it by documented design; it compares URL.Hostname(), which is why the port change is invisible to it, and it does not examine the scheme.",
        "On scheme-downgrade (https to http, same host) Go net/http, Python urllib.request carried Authorization onto a plaintext hop, and the landing response records transport_was_encrypted: false. Nothing is malfunctioning: Go's comparison is a hostname comparison and a scheme change does not change the hostname; urllib makes no comparison at all.",
        "Python urllib (stdlib) forwarded Authorization and Cookie on 8 of 8 header flavors. Its HTTPRedirectHandler copies the caller-set headers with no host comparison of any kind. Credentials its own auth handlers generate go in a separate store that is not copied, so this affects headers the caller set, which is the common case for API clients.",
        "curl restored both headers on boomerang: absent at the subdomain, present again when the chain returned to the origin it started from. curl compares each hop against the ORIGINAL origin and regenerates the header rather than carrying a mutated map. Every other client that stripped at the subdomain stayed stripped on the way back; a client that never stripped at the subdomain has nothing to restore.",
        "Python requests, Python httpx kept Authorization on scheme-upgrade (http to https, same host, default ports) while stripping it at the subdomain; curl, Node fetch (undici), Python urllib3, Python aiohttp stripped it here too. Both carry a carve-out their own source comments describe as backwards compatibility rather than a security judgement. Both also dropped the hand-set Cookie header on EVERY redirect, the same-origin control included, because they clear it before any origin comparison and re-derive from their jar.",
        "port-change: Go net/http, Python urllib.request forwarded to badhttp.dev:8443; curl, Node fetch (undici), Python requests, Python httpx, Python urllib3, Python aiohttp treated the port change as a different origin. relative-authority: every client resolved the protocol-relative Location to alt.badhttp.dev and then applied exactly the policy it applied on to-subdomain (8 of 8 agree with themselves), so no client here showed the resolution-versus-policy split that flavor exists to catch.",
        "jar: of the three cookies the first hop sets before crossing to alt.badhttp.dev, badhttp_domain (Domain=badhttp.dev) arrived from curl, Go net/http, Python urllib.request, Python requests, Python httpx, Python aiohttp. badhttp_hostonly (no Domain attribute) arrived from Python urllib.request, Python requests, Python httpx, and __Host-badhttp_lock from Python urllib.request, Python requests, Python httpx. Those are the three clients built on Python's http.cookiejar, and none of curl, Go or aiohttp: the stdlib's DefaultCookiePolicy returns a cookie set without a Domain attribute to any host under it unless DomainStrictNonDomain is set (the default is DomainLiberal), and cookiejar.py contains no handling of the __Host- or __Secure- prefixes at all; aiohttp's own jar tracks host-only cookies. Node fetch and urllib3 have no jar, and nothing arrived.",
        "Node fetch (undici) stripped Authorization and the Cookie header on 7 of 7 boundary flavors and kept both on the same-origin control — subdomain, port and scheme changes alike, boomerang not restored. That is the Fetch standard's rule, under which an origin is scheme, host and port, applied outside a browser."
      ],
      "freshness": "A dated capture, not a live measurement: taken 2026-09-17T20:56:39Z against badhttp version 0.17.0, with the client versions on each row. Client behaviour changes between releases — several of the advisories /crosshost cites are exactly such changes — and this table does not update itself. Re-run it from scripts/crosshost-witness/ in the source and the numbers move; the date on every row is how you know whether to trust it.",
      "reproduce": "Every row is one chain. Start the row's client at the row's url with Authorization: Basic YWdlbnQ6Y29ycmVjdA== (agent:correct), X-Api-Key: badhttp-key-ok and, on every flavor but jar, Cookie: badhttp_witness=1; let it follow the redirect; read the landing response's `received` object. Pace the chains — each is 2 or 3 requests against a zone limit of 100 per 10 s — and treat a response without x-badhttp-version as no observation."
    },
    "auth": {
      "what": "What eight HTTP clients did with the eighteen /auth flavors when handed the documented fake credentials through their own mechanism for the flavor's scheme, one row per observation. Each row is one call from the caller's point of view: the client was configured, asked for the flavor url, followed any challenge or redirect on its own, and the /auth response it finally handed back is the oracle. What the row adds from the harness's side is how many requests the client sent to get there.",
      "rows": 144,
      "flavors": 18,
      "observed": "2026-09-18T18:18:33Z",
      "badhttp_version_observed": "0.18.0",
      "worker_version": "6fb96efe",
      "clients": [
        {
          "id": "curl",
          "name": "curl",
          "role": "client",
          "version": "8.7.1",
          "platform": "x86_64-apple-darwin26.0",
          "invocation": "curl -sS -v -L --max-redirs 5 --max-time 30 <mechanism> URL",
          "requests_counted_by": "the \"> METHOD target\" lines of curl -v (one per request curl sends, auth retries and followed redirects included); per-hop status from the \"< HTTP/x NNN\" lines"
        },
        {
          "id": "go-nethttp",
          "name": "Go net/http",
          "role": "client",
          "version": "go1.27.0",
          "platform": "darwin/arm64",
          "invocation": "http.Client{Transport: counting RoundTripper, Timeout: 30s, CheckRedirect: stops at 6}",
          "requests_counted_by": "a counting http.RoundTripper (one RoundTrip per request the client sends; net/http never re-sends for auth)"
        },
        {
          "id": "undici",
          "name": "Node fetch (undici)",
          "role": "client",
          "version": "node v26.8.1 / undici 8.10.0",
          "platform": "node darwin/arm64",
          "invocation": "fetch(url, {headers, redirect: 'follow'})",
          "requests_counted_by": "undici's diagnostics channel undici:request:create (one per request the dispatcher sends, each redirect hop dispatched separately) and undici:request:headers for the status"
        },
        {
          "id": "urllib",
          "name": "Python urllib.request",
          "role": "client",
          "version": "python 3.14.7 stdlib",
          "platform": "darwin/arm64",
          "invocation": "build_opener(<auth handler(s)>, HTTPRedirectHandler, counting handler).open(Request(url))",
          "requests_counted_by": "a BaseHandler whose http_request/https_request runs once per request the opener sends (auth handlers and the redirect handler re-enter parent.open); http_response for the status"
        },
        {
          "id": "requests",
          "name": "Python requests",
          "role": "client",
          "version": "2.34.2 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "requests.Session().get(url, auth=<mechanism>, allow_redirects=True)",
          "requests_counted_by": "a counting HTTPAdapter mounted on the Session (sees the original, every redirect hop and the Digest retry, which auth.py sends through r.connection)"
        },
        {
          "id": "httpx",
          "name": "Python httpx",
          "role": "client",
          "version": "0.28.1 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "httpx.Client(follow_redirects=True).get(url, auth=<mechanism>)",
          "requests_counted_by": "event_hooks request/response on the Client (once per wire request inside _send_handling_redirects, auth retries included)"
        },
        {
          "id": "urllib3",
          "name": "Python urllib3",
          "role": "client",
          "version": "2.8.0 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "urllib3.PoolManager().request(\"GET\", url, headers=<mechanism>, redirect=True)",
          "requests_counted_by": "a HTTPSConnectionPool subclass whose _make_request runs once per wire request (redirects re-enter urlopen)"
        },
        {
          "id": "aiohttp",
          "name": "Python aiohttp",
          "role": "client",
          "version": "3.14.3 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "aiohttp.ClientSession(<mechanism>).get(url, allow_redirects=True)",
          "requests_counted_by": "an innermost client middleware called once per wire request (the Digest middleware wraps it; the redirect loop re-runs the chain per hop)"
        }
      ],
      "fields": [
        "id",
        "corpus_id",
        "family",
        "flavor",
        "url",
        "client",
        "observed",
        "badhttp_version_observed",
        "status",
        "reported_error",
        "outcome",
        "license",
        "mechanism_kind",
        "mechanism",
        "credential_encoded_by",
        "requests_made",
        "hops",
        "statuses_seen",
        "credentialed_requests",
        "sent_credentials_first",
        "last_status_seen",
        "redirects_followed",
        "redirect_followed",
        "final_url",
        "attempts",
        "authenticated",
        "checked_flag",
        "scheme_reported",
        "algorithm",
        "encoding",
        "generations",
        "matched",
        "server_error",
        "server_defect",
        "server_note",
        "challenge_seen",
        "error_kind",
        "oracle"
      ],
      "mechanism_legend": {
        "basic-auth": "The client's own Basic option or auth object (curl -u, Go Request.SetBasicAuth, requests/httpx/aiohttp BasicAuth, urllib3 make_headers), configured with the documented credentials. The client encodes them and decides when to send them.",
        "basic-header": "A hand-set Authorization: Basic header, used only for Node fetch, which has no credential mechanism of its own. The HARNESS encoded the credentials (base64 of the UTF-8 bytes), so the encoding on that row is the harness's choice, and the header goes out on the first request by construction.",
        "basic-handler": "A challenge handler for Basic (urllib's HTTPBasicAuthHandler over HTTPPasswordMgrWithDefaultRealm): nothing is sent until a 401 arrives, and the handler decides whether the challenge is one it will answer.",
        "digest-handler": "The client's Digest mechanism (curl --digest, urllib's HTTPDigestAuthHandler, requests' and httpx's DigestAuth, aiohttp's DigestAuthMiddleware), a fresh instance per row: nothing is sent until a Digest challenge arrives. On multi it was handed the choice for requests, httpx and aiohttp, which have no mechanism that chooses among offered schemes, because it must parse the two-challenge header to answer.",
        "any-handler": "A mechanism that reads the challenge and chooses among the schemes it speaks: curl --anyauth (Basic, Digest, NTLM, Negotiate as offered) and urllib with both its Basic and Digest handlers installed on one opener.",
        "bearer-auth": "The client's own Bearer option (curl --oauth2-bearer) with the documented test token.",
        "bearer-header": "A hand-set Authorization: Bearer header with the documented test token: no client here but curl has a Bearer option.",
        "no-mechanism": "The client has no mechanism for the scheme the flavor demands (net/http, fetch and urllib3 have no Digest and no WWW-Authenticate handling), so the request was sent with no credentials at all. The row is a capability, not a bug."
      },
      "outcome_legend": {
        "authenticated": "The final response the caller received was a 2xx whose body says authenticated: true. On accept-any that is the server saying it checked nothing (checked_flag: false).",
        "refused": "A final 4xx from this server reached the caller; server_error, server_defect, server_note and challenge_seen say why. On forbidden the body says the credentials were valid and the resource still refuses; on the Digest flavors under no-mechanism it is the absence of any credentials. Read the flavor.",
        "redirect-not-followed": "A final 3xx reached the caller: the client returned the redirect rather than following it.",
        "client-raised": "The client raised something other than its transport-error type before returning a response to the caller. reported_error names the exception (anything credential-shaped in it was scrubbed); last_status_seen and challenge_seen carry what the harness saw on the wire before the raise.",
        "request-failed": "A transport failure; the client returned nothing usable."
      },
      "reading_this": "Read `outcome` and every server-reported field as a description of what the caller received, never as a verdict on the client. `mechanism_kind` and `mechanism` say what was CONFIGURED for that row — chosen per flavor by the harness, not by the client — and mechanism_legend says which kinds are the client's own (it encoded and sent the credentials) and which are a header the harness set. Whether credentials went out before any challenge is not asserted anywhere: `hops` lists every request the client sent with its status and whether Authorization or Proxy-Authorization was present, `sent_credentials_first` and `credentialed_requests` are read from it, and a count is only meaningful next to the kind — a header kind sends on the first request by construction, a handler kind cannot. A client with no mechanism for the flavor's scheme (`no-mechanism`) was sent with no credentials, so its `refused` on the Digest flavors is a capability of the library, not a bug in it; a client handed its Basic option on a challenge-parser flavor never observed the challenge, and its mechanism string says so. On the proxy flavor the credentials configured were ORIGIN credentials, which that flavor does not read; no proxy credentials were configured because no proxy exists in the harness, so those rows show what a client does with an unexpected 407, not what it would do with proxy credentials. On always-401, `credentialed_requests` is an observation for handler kinds and one-by-construction for the others, and it cannot distinguish a retry with the same credentials from a retry with different ones. Every count is bounded by the client's own retry cap and by the harness's 30-second timeout and 6-redirect limit, so a runaway loop would appear as request-failed, not as a large number; `requests_counted_by` on each client says at which layer its requests were counted. RFC 9110 §11 leaves preemptive sending, retry counts and the choice among several challenges to the client, and RFC 7617 makes charset advisory, so `encoding` is the server reporting which bytes arrived, not a grade — and on a harness-encoded row it reports the harness's choice. Nothing the server received is echoed anywhere: the /auth bodies name only the documented identity, `challenge_seen` is the server's own header, and every client-side error string was scrubbed of anything credential-shaped before it was published.",
      "outcome_counts": {
        "authenticated": 98,
        "refused": 44,
        "client-raised": 2
      },
      "mechanism_counts": {
        "basic-auth": 75,
        "bearer-auth": 1,
        "digest-handler": 18,
        "any-handler": 7,
        "bearer-header": 7,
        "no-mechanism": 9,
        "basic-header": 14,
        "basic-handler": 13
      },
      "disagreement_by_flavor": {
        "stale": {
          "distinct_answers": 2,
          "answers_by_kind": {
            "digest-handler": {
              "401 refused error=unauthorized": [
                "requests",
                "httpx",
                "aiohttp"
              ],
              "200 authenticated scheme=Digest algorithm=MD5 generations=2": [
                "curl",
                "urllib"
              ]
            }
          }
        },
        "utf8": {
          "distinct_answers": 2,
          "answers_by_kind": {
            "basic-auth": {
              "200 authenticated scheme=Basic encoding=utf-8": [
                "curl",
                "go-nethttp",
                "httpx"
              ],
              "200 authenticated scheme=Basic encoding=latin1": [
                "requests",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic encoding=utf-8": [
                "undici"
              ]
            },
            "basic-handler": {
              "200 authenticated scheme=Basic encoding=utf-8": [
                "urllib"
              ]
            }
          }
        },
        "accept-any": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "basic-auth": {
              "200 authenticated scheme=Basic checked=false": [
                "curl",
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic checked=false": [
                "undici"
              ]
            },
            "basic-handler": {
              "200 authenticated scheme=Basic checked=false": [
                "urllib"
              ]
            }
          }
        },
        "always-401": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "basic-auth": {
              "401 refused error=unauthorized": [
                "curl",
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "401 refused error=unauthorized": [
                "undici"
              ]
            },
            "basic-handler": {
              "401 refused error=unauthorized": [
                "urllib"
              ]
            }
          }
        },
        "bare-scheme": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "any-handler": {
              "200 authenticated scheme=Basic": [
                "curl"
              ]
            },
            "basic-auth": {
              "200 authenticated scheme=Basic": [
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic": [
                "undici"
              ]
            },
            "basic-handler": {
              "401 refused error=unauthorized": [
                "urllib"
              ]
            }
          }
        },
        "basic": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "basic-auth": {
              "200 authenticated scheme=Basic": [
                "curl",
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic": [
                "undici"
              ]
            },
            "basic-handler": {
              "200 authenticated scheme=Basic": [
                "urllib"
              ]
            }
          }
        },
        "bearer": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "bearer-auth": {
              "200 authenticated scheme=Bearer": [
                "curl"
              ]
            },
            "bearer-header": {
              "200 authenticated scheme=Bearer": [
                "go-nethttp",
                "undici",
                "urllib",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            }
          }
        },
        "case": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "any-handler": {
              "200 authenticated scheme=Basic": [
                "curl"
              ]
            },
            "basic-auth": {
              "200 authenticated scheme=Basic": [
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic": [
                "undici"
              ]
            },
            "basic-handler": {
              "200 authenticated scheme=Basic": [
                "urllib"
              ]
            }
          }
        },
        "digest": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "digest-handler": {
              "200 authenticated scheme=Digest algorithm=MD5": [
                "curl",
                "urllib",
                "requests",
                "httpx",
                "aiohttp"
              ]
            }
          }
        },
        "digest-sha256": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "digest-handler": {
              "200 authenticated scheme=Digest algorithm=SHA-256": [
                "curl",
                "urllib",
                "requests",
                "httpx",
                "aiohttp"
              ]
            }
          }
        },
        "forbidden": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "basic-auth": {
              "403 refused error=forbidden": [
                "curl",
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "403 refused error=forbidden": [
                "undici"
              ]
            },
            "basic-handler": {
              "403 refused error=forbidden": [
                "urllib"
              ]
            }
          }
        },
        "multi": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "any-handler": {
              "200 authenticated scheme=Digest algorithm=MD5 matched=the Digest challenge (listed first)": [
                "curl",
                "urllib"
              ]
            },
            "basic-auth": {
              "200 authenticated scheme=Basic matched=the Basic challenge (listed second)": [
                "go-nethttp",
                "urllib3"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic matched=the Basic challenge (listed second)": [
                "undici"
              ]
            },
            "digest-handler": {
              "200 authenticated scheme=Digest algorithm=MD5 matched=the Digest challenge (listed first)": [
                "requests",
                "httpx",
                "aiohttp"
              ]
            }
          }
        },
        "none": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "basic-auth": {
              "200 authenticated scheme=Basic": [
                "curl",
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic": [
                "undici"
              ]
            },
            "basic-handler": {
              "401 refused error=unauthorized": [
                "urllib"
              ]
            }
          }
        },
        "proxy": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "basic-auth": {
              "407 refused error=proxy authentication required": [
                "curl",
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "client-raised": [
                "undici"
              ]
            },
            "basic-handler": {
              "407 refused error=proxy authentication required": [
                "urllib"
              ]
            }
          }
        },
        "quoted": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "any-handler": {
              "200 authenticated scheme=Basic": [
                "curl"
              ]
            },
            "basic-auth": {
              "200 authenticated scheme=Basic": [
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic": [
                "undici"
              ]
            },
            "basic-handler": {
              "200 authenticated scheme=Basic": [
                "urllib"
              ]
            }
          }
        },
        "redirect": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "basic-auth": {
              "200 authenticated scheme=Basic": [
                "curl",
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic": [
                "undici"
              ]
            },
            "basic-handler": {
              "200 authenticated scheme=Basic": [
                "urllib"
              ]
            }
          }
        },
        "token68": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "any-handler": {
              "200 authenticated scheme=Basic": [
                "curl"
              ]
            },
            "basic-auth": {
              "200 authenticated scheme=Basic": [
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "200 authenticated scheme=Basic": [
                "undici"
              ]
            },
            "basic-handler": {
              "200 authenticated scheme=Basic": [
                "urllib"
              ]
            }
          }
        },
        "unknown-scheme": {
          "distinct_answers": 1,
          "answers_by_kind": {
            "any-handler": {
              "401 refused error=unauthorized": [
                "curl"
              ]
            },
            "basic-auth": {
              "401 refused error=unauthorized": [
                "go-nethttp",
                "requests",
                "httpx",
                "urllib3",
                "aiohttp"
              ]
            },
            "basic-header": {
              "401 refused error=unauthorized": [
                "undici"
              ]
            },
            "basic-handler": {
              "client-raised": [
                "urllib"
              ]
            }
          }
        }
      },
      "disagreement_note": "distinct_answers is, per flavor, the largest number of distinct answers (what the caller received) among clients configured with the SAME mechanism_kind; answers_by_kind lists them. Clients configured differently are the harness's grid, not a disagreement, and no-mechanism rows are excluded.",
      "findings": [
        "Whether credentials go out before any challenge is read from hops, and it follows the mechanism, not the client: on basic, curl, Go net/http, Node fetch (undici), Python requests, Python httpx, Python urllib3, Python aiohttp sent them on their first request; Python urllib.request sent nothing until the 401 arrived. On none — a 401 with no WWW-Authenticate at all — that decides everything: curl, Go net/http, Node fetch (undici), Python requests, Python httpx, Python urllib3, Python aiohttp authenticated and Python urllib.request could not, because there was no challenge to answer. urllib's HTTPBasicAuthHandler is the only Basic-only challenge handler in this roster, and every other client's Basic option (curl's -u included) sets the header before the first request (the stdlib also offers HTTPPasswordMgrWithPriorAuth for that form, not used here); curl --anyauth, used on the parser flavors, also waits for the challenge.",
        "digest: curl, Python urllib.request, Python requests, Python httpx, Python aiohttp completed the MD5 challenge (401 then 200: a probe without credentials, then the answer). Go net/http, Node fetch (undici), Python urllib3 have no Digest mechanism at all and were sent with no credentials, so their 401 there is a capability of the library, not a bug in it. net/http, fetch and urllib3 have no WWW-Authenticate handling: a 401 is returned to the caller as an ordinary response, and nothing in those libraries reads WWW-Authenticate.",
        "digest-sha256: curl, Python urllib.request, Python requests, Python httpx, Python aiohttp completed the SHA-256 challenge. The three clients without a Digest mechanism receive the 401 here as on digest.",
        "stale: curl, Python urllib.request answered the second 401, the one carrying stale=true, and reached the 200 (generations: 2); Python requests, Python httpx, Python aiohttp handed the stale=true 401 back to the caller after answering one challenge each per call. None of these three reads the stale parameter: each answers exactly one Digest challenge per call, and the second 401 — which promises the credentials were right — consumes that budget. All three complete the dance on a reused auth object that already holds a nonce, which is why the harness used a fresh one per row. The rows do not show whether either of the two reads stale. By its source (Python's urllib/request.py has no mention of stale) urllib does not: its Digest handler answers any new challenge, up to six retries per handler, so what separates it from the three above is the retry budget and not the parameter.",
        "multi (Digest and Basic in one comma-joined header): 2 of 8 clients have a mechanism that reads the list and chooses — curl, Python urllib.request — and both chose Digest. Python requests, Python httpx, Python aiohttp have no such mechanism and were handed their Digest object, which had to parse the two-challenge header to answer: all did, and answered Digest. Go net/http, Node fetch (undici), Python urllib3 were handed their Basic option and never observed the challenge, so scheme_reported: Basic on those rows is the harness's configuration, not a choice the client made. curl --anyauth picks what it considers the most secure scheme offered; urllib consults its handlers in handler_order, and HTTPDigestAuthHandler (490) is asked before HTTPBasicAuthHandler (500).",
        "always-401 (a perfect Basic challenge that rejects everything): every client ended with the 401 in hand. Where a handler was configured (Python urllib.request) — the only rows on which a retry count is an observation — the credentialed attempts were 1. The other 7 sent the credentials on their first request, by the client's own design for a Basic option and by construction for a hand-set header, and made 1 credentialed attempt each: none re-sent after the 401. No client looped.",
        "utf8 (a password with an é under charset=\"UTF-8\"): among the clients that encoded the credentials themselves, curl, Go net/http, Python urllib.request, Python httpx sent the é as UTF-8 and Python requests, Python urllib3, Python aiohttp as Latin-1; Node fetch (undici) carried a header the harness encoded (UTF-8), which is the harness's choice and not an observation of the client. All authenticated: charset is advisory (RFC 7617 §2.1) and this server accepts either encoding, reporting which arrived. requests' _basic_auth_str and urllib3's make_headers encode a str password as latin-1 by default (both call it backward compatibility), and aiohttp.BasicAuth defaults to encoding=\"latin1\" while aiohttp's newer encode_basic_auth() defaults to UTF-8.",
        "proxy (a 407 with Proxy-Authenticate from an origin that is nobody's proxy; no proxy and no proxy credentials were configured, and the Basic credentials on these rows were origin credentials this flavor does not read): no client answered it with the origin credentials — Proxy-Authorization was on none of the requests sent, and the oracle would have said so. curl, Go net/http, Python urllib.request, Python requests, Python httpx, Python urllib3, Python aiohttp handed the 407 back after one request; Node fetch (undici) rejected the call instead (TypeError: fetch failed (cause: Error: \"\")) and the 407 never reached the caller. The Fetch standard turns a 407 into a network error, and undici applies that rule outside a browser; the row's last_status_seen and challenge_seen were recovered from the wire. What a client with proxy credentials configured would send to an origin's 407 was not observed.",
        "The challenge-parser flavors (bare-scheme, unknown-scheme, token68, case, quoted) only exercise a client that reads the challenge, and for a Basic-shaped challenge that is curl, Python urllib.request here (a handler kind on those rows). bare-scheme: answered by curl, declined by Python urllib.request (401 returned, credentials never sent); token68: answered by curl, Python urllib.request; case: answered by curl, Python urllib.request; quoted: answered by curl, Python urllib.request; unknown-scheme: Python urllib.request raised after the 401 had already arrived (last_status_seen: 401), curl returned the 401 without retrying. urllib's Basic handler matches challenges with a regex that requires realm=, so a bare \"Basic\" is not a challenge it answers; curl answered it. The 6 clients handed their Basic option or a header sent it on their first request and never saw the challenge, so the server decided those rows before any parser ran: authenticated on bare-scheme, token68, case, quoted, 401 on unknown-scheme for all of them.",
        "redirect (302 to /auth/basic, same host, same scheme): all 8 reached the 200: 302 then 200 with credentials on 2 of 2 requests (curl, Go net/http, Node fetch (undici), Python requests, Python httpx, Python urllib3, Python aiohttp); 302 then 401 then 200 with credentials on 1 of 3 requests (Python urllib.request). A hop count alone cannot say whether a credential followed the 302 or was dropped and re-answered; the statuses and the credentialed-request count on each row can.",
        "accept-any: all 8 received authenticated: true with checked: false — the server checked nothing, and a client that authenticates here has shown only that it sent a header. bearer: all 8 authenticated in one request; only curl has a Bearer option, the rest carried a header. forbidden: all 8 received the 403 after one credentialed attempt and none retried.",
        "2 of 144 observations ended in the client raising rather than returning a response: Node fetch (undici) on proxy, Python urllib.request on unknown-scheme. 0 of 144 observations ended in a transport failure with nothing usable returned. Every other row is a response this server sent, read back from the wire with its x-badhttp-version, and is one request or more that this server answered."
      ],
      "freshness": "A dated capture, not a live measurement: taken 2026-09-18T18:18:33Z against badhttp version 0.18.0, with the client versions on each row. Client behaviour changes between releases and this table does not update itself. Re-run it from scripts/auth-witness/ in the source and the numbers move; the date on every row is how you know whether to trust it.",
      "reproduce": "Every row is one call. Configure the row's client the way its mechanism field says, with the documented credentials from GET /auth and nothing else, ask for the row's url, and compare the status and body you get back with the row's status and server-reported fields; count the requests your client sent to get there. Pace the calls against the zone limit of 100 per 10 s, and treat a response without x-badhttp-version as no observation."
    },
    "cookies": {
      "what": "What eight HTTP clients did with the seventeen /cookies setter flavors, one row per observation. Each row is four requests with one fresh jar where the client has one (requests_made and hops count the setter fetch and its redirects only): the setter (redirects followed), /cookies/echo, /cookies/delete, and /cookies/echo again. The oracle is /cookies/echo — the Cookie header exactly as it reached the Worker — and what the row adds from the harness's side is the jar's own record where the client exposes one.",
      "rows": 136,
      "flavors": 17,
      "observed": "2026-09-30T15:46:06Z",
      "badhttp_version_observed": "0.19.0",
      "worker_version": "794931ee",
      "clients": [
        {
          "id": "curl",
          "name": "curl",
          "role": "client",
          "version": "8.7.1",
          "platform": "x86_64-apple-darwin26.0",
          "invocation": "curl -q -sS -v -L --max-redirs 5 --max-time 30 -c JAR -b JAR URL (fresh jar file per flavor; steps 2-4 without -L)",
          "jar_kind": "own-jar",
          "jar": "curl's own cookie engine: a fresh Netscape-format jar file per flavor, read (-b) and written (-c) on all four requests"
        },
        {
          "id": "go-nethttp",
          "name": "Go net/http",
          "role": "client",
          "version": "go1.27.0",
          "platform": "darwin/arm64",
          "invocation": "go run scripts/cookies-witness/go-nethttp.go, from a throwaway module (go mod init + go get golang.org/x/net/publicsuffix); http.Client{Jar: cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}), Transport: counting RoundTripper, Timeout: 30s, CheckRedirect: stops at 6}",
          "jar_kind": "own-jar",
          "jar": "net/http/cookiejar.Jar created with cookiejar.Options{PublicSuffixList: golang.org/x/net/publicsuffix.List}, a fresh jar per flavor, attached as http.Client.Jar"
        },
        {
          "id": "undici",
          "name": "Node fetch (undici) + tough-cookie",
          "role": "client",
          "version": "node v26.10.0 / undici 8.10.2 / tough-cookie 6.0.2",
          "platform": "node darwin/arm64",
          "invocation": "fetch(url, {headers: {Cookie: await jar.getCookieString(url)}, redirect: 'manual'}) x4 per flavor, harness follows <= 6 redirects and feeds response.headers.getSetCookie() to jar.setCookie(str, url); tough-cookie installed with npm and loaded via createRequire from $COOKIE_NODEMODS",
          "jar_kind": "harness-jar",
          "jar": "tough-cookie 6.0.2 CookieJar, default options (MemoryCookieStore, rejectPublicSuffixes true, looseMode false, allowSpecialUseDomain true); a fresh jar per flavor; the harness, not fetch, copies Set-Cookie into the jar and the jar into Cookie"
        },
        {
          "id": "urllib",
          "name": "Python urllib.request",
          "role": "client",
          "version": "python 3.14.7 stdlib",
          "platform": "darwin/arm64",
          "invocation": "build_opener(HTTPCookieProcessor(CookieJar()), counting handler).open(Request(url)) x4",
          "jar_kind": "own-jar",
          "jar": "http.cookiejar.CookieJar() default policy (DefaultCookiePolicy, no explicit blocked/allowed domains) behind urllib.request.HTTPCookieProcessor, one fresh jar and opener per flavor"
        },
        {
          "id": "requests",
          "name": "Python requests",
          "role": "client",
          "version": "2.34.2 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "requests.Session().get(url, allow_redirects=True) x4",
          "jar_kind": "own-jar",
          "jar": "requests.Session() default RequestsCookieJar (a http.cookiejar.CookieJar subclass, DefaultCookiePolicy), one fresh Session per flavor; trust_env=False"
        },
        {
          "id": "httpx",
          "name": "Python httpx",
          "role": "client",
          "version": "0.28.1 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "httpx.Client(follow_redirects=True).get(url) x4",
          "jar_kind": "own-jar",
          "jar": "httpx.Client() default httpx.Cookies (wraps a http.cookiejar.CookieJar, DefaultCookiePolicy), one fresh Client per flavor; trust_env=False"
        },
        {
          "id": "urllib3",
          "name": "Python urllib3",
          "role": "client",
          "version": "2.8.0 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "urllib3.PoolManager().request(\"GET\", url, redirect=True) x4, no jar",
          "jar_kind": "no-jar",
          "jar": "none: urllib3.PoolManager() has no cookie jar, so no Cookie header is ever sent and Set-Cookie is only visible on the response"
        },
        {
          "id": "aiohttp",
          "name": "Python aiohttp",
          "role": "client",
          "version": "3.14.3 (python 3.14.7)",
          "platform": "darwin/arm64",
          "invocation": "aiohttp.ClientSession().get(url, allow_redirects=True) x4",
          "jar_kind": "own-jar",
          "jar": "aiohttp.ClientSession() default aiohttp.CookieJar() (unsafe=False, RFC 6265 domain/path matching with its own date parser), one fresh session per flavor"
        }
      ],
      "fields": [
        "id",
        "corpus_id",
        "family",
        "flavor",
        "url",
        "client",
        "observed",
        "badhttp_version_observed",
        "status",
        "reported_error",
        "outcome",
        "license",
        "jar_kind",
        "jar",
        "requests_made",
        "hops",
        "setter_status",
        "setter_body_read",
        "planted_names",
        "echo",
        "returned_names",
        "unplanted_names",
        "after_delete",
        "after_delete_remaining",
        "after_delete_unplanted",
        "jar_enumerable",
        "jar_entries",
        "jar_rejections",
        "attempts",
        "error_kind",
        "oracle"
      ],
      "jar_legend": {
        "own-jar": "The client's own cookie jar, default configuration, one fresh jar per flavor (curl -c/-b with a fresh file; net/http/cookiejar with the public suffix list; http.cookiejar behind urllib, requests and httpx; aiohttp's CookieJar). The client decides what to store and what to send.",
        "harness-jar": "A jar the harness supplies because the transport has none: Node fetch with tough-cookie, the harness copying Set-Cookie into the jar and the jar's Cookie string onto each request, redirects followed by the harness so the 302's own Set-Cookie is seen. What is stored and what is sent is tough-cookie's decision; when it was sent is the harness's.",
        "no-jar": "The client has no cookie jar at all (urllib3's PoolManager): Set-Cookie is visible on the response and nothing is ever sent back. Every such row returns nothing, which is a capability of the library, not a bug in it."
      },
      "outcome_legend": {
        "all-returned": "Every cookie the flavor planted came back on the follow-up to /cookies/echo (the two nameless cookies are matched by value).",
        "some-returned": "At least one planted cookie came back and at least one did not.",
        "none-returned": "None came back. On wrong-domain and path-prefix this is the specified answer; on public-suffix it is what a jar with a public-suffix list returns (RFC 6265 §5.3 step 5 says user agents SHOULD use one, and plain RFC 6265 without a list would store it); on a no-jar client it is what having no jar means; elsewhere it is the finding.",
        "client-raised": "The client raised something other than its transport-error type before returning a response.",
        "request-failed": "The client raised before any usable response reached the caller, or the transport failed."
      },
      "reading_this": "Read outcome as a description of what came back to /cookies/echo relative to what the flavor planted, never as a verdict on the client. \"none-returned\" is the specified answer on wrong-domain and path-prefix, the answer of a jar that uses a public-suffix list on public-suffix (RFC 6265 recommends one; without it the cookie is stored), and the finding elsewhere, and which is which is a property of the flavor (GET /cookies says so per flavor). A jar_kind of no-jar returns nothing everywhere because there is nothing to return from. unplanted_names lists cookies that came back which this flavor did not set (a comma-splitting jar's invented second cookie, a value-with-no-equals stored as a NAME), and after_delete_unplanted lists what was still there after /cookies/delete that the flavor never planted (a deletion tombstone kept as a live cookie). jar_entries is what the client's jar recorded where the harness could enumerate it, which is how a cookie stored and then withheld is told apart from one never stored. No client here is scored, ranked, or called conformant; RFC 6265 and rfc6265bis disagree with each other on several of these flavors, and a jar can be conformant to either.",
      "outcome_counts": {
        "all-returned": 85,
        "none-returned": 43,
        "some-returned": 7,
        "client-raised": 1
      },
      "jar_counts": {
        "own-jar": 102,
        "harness-jar": 17,
        "no-jar": 17
      },
      "disagreement_by_flavor": {
        "folded": {
          "distinct_answers": 3,
          "answers": {
            "all-returned | returned badhttp_folded_a=1, badhttp_folded_b=2": [
              "curl",
              "undici",
              "urllib",
              "requests",
              "httpx"
            ],
            "all-returned | returned badhttp_folded_a=\"1, badhttp_folded_b=2\"": [
              "go-nethttp"
            ],
            "all-returned | returned badhttp_folded_a=\"1\\054\"; badhttp_folded_b=2": [
              "aiohttp"
            ]
          }
        },
        "name-prefixes": {
          "distinct_answers": 3,
          "answers": {
            "all-returned | returned __Host-badhttp_bad=1; __Secure-badhttp_good=1; __Secure-badhttp_bad=1; __Host-badhttp_good=1": [
              "go-nethttp",
              "urllib",
              "requests",
              "httpx"
            ],
            "some-returned | returned __Secure-badhttp_good=1; __Host-badhttp_good=1": [
              "curl",
              "undici"
            ],
            "all-returned | returned __Host-badhttp_bad=1; __Host-badhttp_good=1; __Secure-badhttp_bad=1; __Secure-badhttp_good=1": [
              "aiohttp"
            ]
          }
        },
        "quoted": {
          "distinct_answers": 3,
          "answers": {
            "all-returned | returned badhttp_quoted=\"hello world\"; badhttp_semi=\"semi": [
              "curl",
              "undici",
              "urllib",
              "requests",
              "httpx"
            ],
            "some-returned | returned badhttp_quoted=\"hello world\"": [
              "go-nethttp"
            ],
            "all-returned | returned badhttp_quoted=\"hello world\"; badhttp_semi=\"semi; =colon\"": [
              "aiohttp"
            ]
          }
        },
        "utf8": {
          "distinct_answers": 3,
          "answers": {
            "all-returned | returned badhttp_utf8=☃": [
              "curl",
              "undici",
              "urllib",
              "requests"
            ],
            "none-returned | returned nothing": [
              "go-nethttp",
              "aiohttp"
            ],
            "client-raised": [
              "httpx"
            ]
          }
        },
        "domain": {
          "distinct_answers": 2,
          "answers": {
            "all-returned | returned badhttp_domain_dot=1; badhttp_domain=1": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx"
            ],
            "all-returned | returned badhttp_domain=1; badhttp_domain_dot=1": [
              "aiohttp"
            ]
          }
        },
        "duplicate": {
          "distinct_answers": 2,
          "answers": {
            "all-returned | returned badhttp_dup=deep; badhttp_dup=shallow": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx"
            ],
            "some-returned | returned badhttp_dup=deep": [
              "aiohttp"
            ]
          }
        },
        "nameless": {
          "distinct_answers": 2,
          "answers": {
            "none-returned | returned nothing": [
              "curl",
              "go-nethttp",
              "undici",
              "aiohttp"
            ],
            "some-returned | returned =badhttp-just-a-value": [
              "urllib",
              "requests",
              "httpx"
            ]
          }
        },
        "public-suffix": {
          "distinct_answers": 2,
          "answers": {
            "none-returned | returned nothing": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx"
            ],
            "all-returned | returned badhttp_supercookie=reject-me": [
              "aiohttp"
            ]
          }
        },
        "bad-expires": {
          "distinct_answers": 1,
          "answers": {
            "all-returned | returned badhttp_bad_expires=1": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        },
        "conflicting-expiry": {
          "distinct_answers": 1,
          "answers": {
            "all-returned | returned badhttp_conflict=alive": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        },
        "far-future": {
          "distinct_answers": 1,
          "answers": {
            "all-returned | returned badhttp_far_future=1": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        },
        "huge": {
          "distinct_answers": 1,
          "answers": {
            "all-returned | returned badhttp_huge=<4084 bytes>": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        },
        "many": {
          "distinct_answers": 1,
          "answers": {
            "all-returned | returned badhttp_many_01=1; badhttp_many_02=1; badhttp_many_03=1; badhttp_many_04=1; badhttp_many_05=1; badhttp_many_06=1; badhttp_many_07=1; badhttp_many_08=1; badhttp_many_09=1; badhttp_many_10=1": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        },
        "ok": {
          "distinct_answers": 1,
          "answers": {
            "all-returned | returned badhttp_ok=1": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        },
        "on-redirect": {
          "distinct_answers": 1,
          "answers": {
            "all-returned | returned badhttp_redirect=1": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        },
        "path-prefix": {
          "distinct_answers": 1,
          "answers": {
            "none-returned | returned nothing": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        },
        "wrong-domain": {
          "distinct_answers": 1,
          "answers": {
            "none-returned | returned nothing": [
              "curl",
              "go-nethttp",
              "undici",
              "urllib",
              "requests",
              "httpx",
              "aiohttp"
            ]
          }
        }
      },
      "disagreement_note": "distinct_answers is, per flavor, the number of distinct answers (what came back to /cookies/echo) among clients that have a jar; answers lists them. What survived /cookies/delete is not in the key — it is reported once, in the findings — and no-jar rows are excluded: returning nothing is what having no jar means.",
      "findings": [
        "ok (the control): all 7 clients with a jar stored badhttp_ok and sent it back to /cookies/echo. Python urllib3 has no cookie jar at all, so its echo carried nothing on every flavor: a capability of the library, not a bug, and its rows say jar_kind: no-jar.",
        "folded (two cookies comma-joined into one Set-Cookie): curl, Go net/http, Node fetch (undici) + tough-cookie, Python urllib.request, Python requests, Python httpx stored one cookie, badhttp_folded_a, with the comma and the fake second cookie inside its value, as the bis parse says; Python aiohttp split on the comma and returned a second cookie named badhttp_folded_b that this server never set.",
        "many (ten separate Set-Cookie headers): all 7 clients with a jar returned all ten, all of them in creation order. duplicate (the same name on two paths): curl, Go net/http, Node fetch (undici) + tough-cookie, Python urllib.request, Python requests, Python httpx sent both badhttp_dup cookies to /cookies/echo, every one with the longer path (deep) first as §5.4 asks; the rest: only deep (Python aiohttp).",
        "on-redirect (Set-Cookie on the 302 itself): all 7 clients with a jar kept the cookie set on the redirect and presented it on the follow-up.",
        "conflicting-expiry (Expires in 1970 and Max-Age=3600 on one cookie): all 7 clients with a jar kept the cookie, so Max-Age won. For Python aiohttp, the Set-Cookie Expires text its Morsel exposes (the jar's own expiry was not recorded in this capture) reads 1970 while the cookie was still sent. bad-expires (an ISO 8601 date): all 7 clients with a jar sent it back; in the jar it is a session cookie (curl, Node fetch (undici) + tough-cookie, Python urllib.request, Python requests, Python httpx); jar not enumerable (Go net/http); expiry not exposed by the jar (Python aiohttp).",
        "far-future (Expires in the year 9999; bis §5.5 recommends a 400-day cap): all 7 clients with a jar sent it back; in the jar: the year 9999 kept (curl, Node fetch (undici) + tough-cookie, Python urllib.request, Python requests, Python httpx); jar not enumerable (Go net/http); the Set-Cookie Expires text its Morsel exposes (the jar's own expiry was not recorded in this capture): Fri, 01 Jan 9999 00:00:00 GMT (Python aiohttp).",
        "wrong-domain (Domain=example.com) and public-suffix (Domain=dev): no client returned the wrong-domain cookie; Python aiohttp returned the supercookie scoped to the whole TLD. Returning nothing is what §5.3 step 6 requires on wrong-domain; on public-suffix it is what a jar with a public-suffix list does (§5.3 step 5, which says user agents SHOULD use one), and plain RFC 6265 without a list would store it.",
        "domain (Domain=.badhttp.dev and Domain=badhttp.dev): all 7 clients with a jar returned both; of the 6 whose jar the harness could enumerate, all recorded both as domain cookies (host_only: false), the leading dot making no difference. path-prefix (Path=/cookie, one letter short): no client sent it to /cookies/echo (/cookie does not path-match /cookies/echo); every enumerable jar had stored it.",
        "name-prefixes (__Host- and __Secure-, one valid and one invalid each): returned __Host-badhttp_good and __Host-badhttp_bad and __Secure-badhttp_good and __Secure-badhttp_bad (Go net/http, Python urllib.request, Python requests, Python httpx, Python aiohttp); returned __Host-badhttp_good and __Secure-badhttp_good (curl, Node fetch (undici) + tough-cookie). The two-cookie answer is the bis §4.1.3 rule; the four-cookie answer is RFC 6265 without prefix rules, so the two answers follow two different specifications.",
        "quoted (a DQUOTE-wrapped value with a space, and a semicolon inside quotes): badhttp_quoted came back as \"hello world\" and badhttp_semi as \"semi (curl, Node fetch (undici) + tough-cookie, Python urllib.request, Python requests, Python httpx, Python aiohttp); badhttp_quoted came back as \"hello world\" and badhttp_semi not returned (Go net/http).",
        "utf8 (a raw ☃, bytes e2 98 83, in the value): bytes e2 98 83 (curl, Node fetch (undici) + tough-cookie, Python urllib.request, Python requests); not returned (Go net/http, Python aiohttp); Python httpx raised after the setter response arrived (status 200, 1 Set-Cookie), with no echo recorded ('ascii' codec can't encode character '\\u2603' in position 13: ordinal not in range(128)).",
        "nameless (a Set-Cookie with no \"=\" at all, and one starting with \"=\"): Python urllib.request, Python requests, Python httpx sent the no-equals line back bare, which this server reads as a nameless value — and their jars record it as a cookie NAMED badhttp-just-a-value with no value at all; no client sent back the line that starts with \"=\"; Node fetch (undici) + tough-cookie refused both loudly (jar_rejections on the row); curl, Python aiohttp stored neither and raised nothing; Go net/http returned neither and raised nothing (jar not enumerable, so what it stored is not visible). http.cookiejar parses a value with no \"=\" as a name and writes it back without one (the same behaviour v0.6.0 recorded on 2026-08-23), which is why /cookies/delete carries a deletion for that name.",
        "huge (name+value exactly 4096 bytes, the bis §5.6 limit): all 7 clients with a jar stored and returned it.",
        "delete (one expiring Set-Cookie per plantable cookie, exact attributes): after the cleanup, curl, Go net/http, Node fetch (undici) + tough-cookie, Python urllib.request, Python requests, Python httpx had none of the planted cookies left on any flavor where the cleanup ran; the cleanup was not observed for Python httpx on utf8; still present afterwards: Python aiohttp (ok).",
        "Also after the cleanup, on the flavors where it ran: Python aiohttp (badhttp_ok) carried a cookie that no flavor in that session had planted — a deletion tombstone from /cookies/delete stored as a live cookie instead of removing one. badhttp_ok is the one deletion in that list that uses the Expires=1970 idiom; the other 44 use Max-Age=0 and took effect wherever the cleanup ran, so the jar that kept it acted on Max-Age and not on an Expires in the past.",
        "1 of 136 observations ended in the client raising or the transport failing: Python httpx on utf8. Every other row is four responses this server sent, each read back from the wire with its x-badhttp-version."
      ],
      "freshness": "A dated capture, not a live measurement: taken 2026-09-30T15:46:06Z against badhttp version 0.19.0, with the client versions on each row. Client behaviour changes between releases and this table does not update itself. Re-run it from scripts/cookies-witness/ in the source and the numbers move; the date on every row is how you know whether to trust it.",
      "reproduce": "Start the row's client with a fresh jar where it has one, GET the row's url following redirects, GET /cookies/echo and compare its cookies with the row's echo.cookies; then GET /cookies/delete and /cookies/echo again and compare with after_delete. Pace the calls against the zone limit of 100 per 10 s, and treat a response without x-badhttp-version as no observation."
    },
    "sse": {
      "what": "What 7 real SSE client libraries did with the fourteen /sse flavors, one row per observation, plus one raw-wire control row per flavor (curl). Each row is one fresh client pointed at GET /sse/{flavor} with default parameters: every event it delivered, every connection it opened and what that connection carried, every error it surfaced, and how it ended. The oracle is the reference WHATWG parse of the bytes the control received.",
      "rows": 112,
      "flavors": 14,
      "observed": "2026-10-05T03:43:48Z",
      "badhttp_version_observed": "0.21.0",
      "worker_version": "78ef72fd",
      "clients": [
        {
          "id": "curl",
          "name": "curl",
          "role": "control",
          "class": "raw",
          "version": "8.7.1",
          "library": "curl (libcurl), raw bytes, no SSE parser",
          "platform": "(x86_64-apple-darwin26.0)",
          "invocation": "curl -sS -N --http1.1 --max-time 30 -D headers -o body URL (no SSE parsing; the de-chunked body bytes are the observation)",
          "connections_counted_by": "one curl invocation is one connection"
        },
        {
          "id": "aiohttp-sse-client",
          "name": "aiohttp-sse-client",
          "role": "client",
          "class": "eventsource",
          "version": "0.2.1",
          "library": "aiohttp-sse-client 0.2.1 over aiohttp 3.14.3",
          "platform": "python 3.14.7 on darwin/arm64",
          "invocation": "HTTP/1.1 (aiohttp speaks nothing else): async with aiohttp_sse_client.client.EventSource(url, session=aiohttp.ClientSession(trace_configs=[...], trust_env=False), on_error=...) as es: async for event in es; one fresh session per flavor; the harness cancels the iteration when the library signals the end of its first connection (every flavor but resume); retry_ms_adopted and last_event_id_final are read from the private _reconnection_time and _last_event_id (the library has no public accessor)",
          "connections_counted_by": "aiohttp TraceConfig (on_request_start, on_request_end, on_request_exception): each request the session issues is one connection, the library's own reconnects included"
        },
        {
          "id": "eventsource-npm",
          "name": "eventsource (npm)",
          "role": "client",
          "class": "eventsource",
          "version": "5.1.2",
          "library": "eventsource 5.1.2 (eventsource-parser 4.1.1) over Node v26.10.0 global fetch (undici 8.10.2), HTTP/1.1 forced with an undici Agent {allowH2: false}",
          "platform": "node darwin/arm64",
          "invocation": "new EventSource(url, {fetch: wrapper}) with eventsource 5.1.2 loaded by import() from NODE_PATH; the wrapper calls the global fetch(url, {...init, dispatcher: new Agent({allowH2: false})}) and re-wraps response.body; one instance and one Agent per flavor, dispatchEvent wrapped per instance to see every event type; closed by the harness at the first error event except on resume",
          "connections_counted_by": "the fetch constructor option: one call of the wrapper is one connection attempt"
        },
        {
          "id": "go-sse",
          "name": "go-sse",
          "role": "client",
          "class": "eventsource",
          "version": "v0.11.0",
          "library": "go-sse (tmaxmax/go-sse), Connection.Connect: reconnects by itself after the stream ends, adopts the server's retry; default retry behavior, Backoff.Jitter 1e-9 so OnRetry reports the adopted retry unjittered",
          "platform": "go1.27.0 darwin/arm64",
          "invocation": "Go program run with go run from a module directory; sse.Client{HTTPClient: http.Client{Transport: counting RoundTripper}, Backoff.Jitter 1e-9, OnRetry}.NewConnection(GET request carrying a 30 s context).Connect(), SubscribeToAll collecting events; stopped from OnRetry after the first connection on every flavor but resume, where it reconnects until it stops by itself or reaches 6 connections",
          "connections_counted_by": "a custom http.RoundTripper wrapping an http.Transport and injected into the library's http.Client; one RoundTrip is one connection, and the response body is wrapped to record how it ended. The Transport is forced to HTTP/1.1 (TLSNextProto set to an empty map) and the protocol of every response is checked"
        },
        {
          "id": "httpx-sse",
          "name": "httpx-sse",
          "role": "client",
          "class": "one-shot",
          "version": "0.4.3",
          "library": "httpx-sse 0.4.3 over httpx 0.28.1",
          "platform": "python 3.14.7 on darwin/arm64",
          "invocation": "HTTP/1.1 (http2=False): httpx_sse.connect_sse(httpx.Client(http1=True, http2=False, trust_env=False, follow_redirects=False), \"GET\", url) then EventSource.iter_sse(); one fresh Client per flavor; connect_sse adds Accept: text/event-stream and Cache-Control: no-store; retry_ms_adopted is the last ServerSentEvent.retry seen, last_event_id_final the last ServerSentEvent.id",
          "connections_counted_by": "httpx event_hooks (request and response): each request the Client sends is one connection; redirects are not followed"
        },
        {
          "id": "node-eventsource",
          "name": "Node built-in EventSource (undici)",
          "role": "client",
          "class": "eventsource",
          "version": "node v26.10.0 / undici 8.10.2",
          "library": "Node.js v26.10.0 built-in EventSource (undici 8.10.2 lib/web/eventsource, enabled with --experimental-eventsource), HTTP/1.1 forced with an undici Agent {allowH2: false}",
          "platform": "node darwin/arm64",
          "invocation": "node --experimental-eventsource --disable-warning=UNDICI-ES (the harness re-executes itself with them when the global is absent): new EventSource(url, {dispatcher: new Agent({allowH2: false}).compose(interceptor)}), one instance and one Agent per flavor, dispatchEvent wrapped per instance to see every event type; closed by the harness at the first error event except on resume",
          "connections_counted_by": "an undici dispatcher interceptor (Agent.compose) on the Agent passed as the EventSource dispatcher option: one dispatch call is one connection attempt"
        },
        {
          "id": "r3labs-sse",
          "name": "r3labs/sse",
          "role": "client",
          "class": "one-shot",
          "version": "v2.10.0",
          "library": "r3labs/sse v2, Client.SubscribeRawWithContext: one call consumes one response and returns after a clean end (no reconnection after a close); configured with ReconnectStrategy = StopBackOff because its default ExponentialBackOff retries forever after any error and ignores the context",
          "platform": "go1.27.0 darwin/arm64",
          "invocation": "Go program run with go run from a module directory; sse.NewClient(url) with Connection = http.Client{Transport: counting RoundTripper} and ReconnectStrategy = backoff.StopBackOff, SubscribeRawWithContext(30 s context, handler) collecting events",
          "connections_counted_by": "a custom http.RoundTripper wrapping an http.Transport and injected into the library's http.Client; one RoundTrip is one connection, and the response body is wrapped to record how it ended. The Transport is forced to HTTP/1.1 (TLSNextProto set to an empty map) and the protocol of every response is checked"
        },
        {
          "id": "sseclient-py",
          "name": "sseclient-py",
          "role": "client",
          "class": "one-shot",
          "version": "1.9.0",
          "library": "sseclient-py 1.9.0 over requests 2.34.2",
          "platform": "python 3.14.7 on darwin/arm64",
          "invocation": "HTTP/1.1 (requests speaks nothing else): sseclient.SSEClient(requests.Session().get(url, stream=True, headers={\"Accept\": \"text/event-stream\"}, allow_redirects=False)).events(); one fresh Session per flavor with trust_env=False; the Response is handed over as the event source; the library keeps no last event id (null) and exposes retry only on a dispatched event",
          "connections_counted_by": "requests HTTPAdapter subclass (send): each send is one connection; redirects are not followed"
        }
      ],
      "fields": [
        "id",
        "corpus_id",
        "family",
        "flavor",
        "url",
        "client",
        "observed",
        "badhttp_version_observed",
        "status",
        "reported_error",
        "outcome",
        "license",
        "class",
        "connections",
        "events",
        "events_delivered",
        "events_note",
        "preamble_events",
        "errors",
        "end",
        "retry_ms_adopted",
        "last_event_id_final",
        "wall_ms",
        "attempts",
        "diff",
        "reference",
        "control",
        "oracle"
      ],
      "class_legend": {
        "eventsource": "Implements the WHATWG EventSource interface: auto-reconnects after the server closes (after retry ms, sending Last-Event-ID), stops on a 204, fails the connection on a Content-Type other than text/event-stream, and surfaces both a clean close and a transport failure as an error event — the interface cannot tell them apart. The expected end is therefore \"reconnecting\" after a clean server close, \"reconnecting\" or \"error\" after drop, \"error\" with no events on wrong-type, and \"stopped\" on resume.",
        "one-shot": "Iterates ONE response as a stream of events and returns when it ends. No reconnection is part of the library, so on resume it delivers ids 1–3 over one connection and returns, which is its design; whether it checks Content-Type is a library choice the specification does not constrain.",
        "raw": "The control: curl receiving the bytes with no SSE parser at all. Its control.raw_sha256 is the hash of the bytes the parser derived the reference from."
      },
      "outcome_legend": {
        "as-spec": "The events delivered are what the client's class is expected to deliver, and the row ended as expected for the class. Expected events are the reference WHATWG parse of the bytes the control received (after removing the server clock \"t\" from JSON data), except that the EventSource interface delivers nothing on wrong-type and, on resume, also the events 4-6 of the documented later connections, and a one-shot library on wrong-type may deliver either.",
        "events-differ": "The row ended as expected but the delivered events differ from the reference: fewer, more, or altered values. diff names the first difference.",
        "end-differs": "The events match but the ending does not (a reset reported as a clean end, a stream that never returned, no reconnection where the class reconnects). diff says which.",
        "both-differ": "Both the events and the ending differ from the reference.",
        "harness-timeout": "The harness's 30 s per-row cap fired; the row records whatever was delivered before it.",
        "request-failed": "No observation: the first response never carried x-badhttp-version after three attempts, or the transport failed before any response.",
        "control": "The curl control row: bytes, not events. Its events are the reference parse of those bytes."
      },
      "reading_this": "Read outcome as a description of what the library delivered to its caller relative to the reference WHATWG HTML §9.2.6 parse of the bytes the control received on the same date, and relative to the library's CLASS (client.class). A one-shot library ending \"clean\" with three events on resume is as-spec for its class: it has no reconnect and says so. An EventSource-interface library ending \"reconnecting\" after a clean close is as-spec for its class: the interface reconnects after every close and cannot distinguish a close from a failure. \"differs\" never means defective — on wrong-type, delivering the three events is a choice the specification leaves to a one-shot library; on error-event, routing a named \"error\" event to the error callback is what the EventSource interface does by design. diff on each row names the first difference, events carries what was delivered (data as delivered, with the server clock \"t\" left in; truncated past 200 bytes, where data_bytes is the length of the full data and data_sha256 its hash; for data of 200 bytes or less data_sha256 is null), connections counts every connection the library opened and what each carried, and end is how the row finished from the caller's side. No library here is scored or ranked, and none is handed a conformance verdict.",
      "outcome_counts": {
        "control": 14,
        "as-spec": 88,
        "events-differ": 8,
        "end-differs": 1,
        "both-differ": 1
      },
      "class_counts": {
        "raw": 1,
        "eventsource": 4,
        "one-shot": 3
      },
      "reference": {
        "ok": {
          "flavor": "ok",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 304,
          "raw_sha256": "14cebb9d8dc3bc939f24893698460ce9f4c2c0b5eb049aa34fd4ca2823063c37",
          "events": [
            {
              "type": "tick",
              "id": "1",
              "data": "{\"n\":1,\"of\":5,\"t\":1791171828419}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "2",
              "data": "{\"n\":2,\"of\":5,\"t\":1791171828419}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "3",
              "data": "{\"n\":3,\"of\":5,\"t\":1791171828669}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "4",
              "data": "{\"n\":4,\"of\":5,\"t\":1791171828919}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "5",
              "data": "{\"n\":5,\"of\":5,\"t\":1791171829169}",
              "data_bytes": 32,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "5",
          "last_event_id_buffer_at_eof": "5",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "stall": {
          "flavor": "stall",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 47,
          "raw_sha256": "5738b03170b34c009488855071965cfb438e8d293179a7259d1349a278568fbe",
          "events": [
            {
              "type": "tick",
              "id": "1",
              "data": "{\"n\":1}",
              "data_bytes": 7,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "1",
          "last_event_id_buffer_at_eof": "1",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "cut": {
          "flavor": "cut",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 61,
          "raw_sha256": "8a53d0382a5b33b453deb005985dad25ec855a9a2da3f418a8e4811c17ea8787",
          "events": [
            {
              "type": "message",
              "id": "1",
              "data": "complete",
              "data_bytes": 8,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "1",
          "last_event_id_buffer_at_eof": "2",
          "retry_ms": 30000,
          "discarded_pending_data": true,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "drop": {
          "flavor": "drop",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 53,
          "raw_sha256": "f1f27f610f89304623c117b063642157f54992f880b7a270a4669bda24c1b01e",
          "events": [
            {
              "type": "tick",
              "id": "1",
              "data": "{\"n\":1}",
              "data_bytes": 7,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "1",
          "last_event_id_buffer_at_eof": "2",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 18,
          "connections_expected": 1
        },
        "crlf": {
          "flavor": "crlf",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 202,
          "raw_sha256": "bcb877e8357193880882fb989cd28638fe5ed17b97ec123af5d0b0f04a0e2498",
          "events": [
            {
              "type": "tick",
              "id": "1",
              "data": "{\"n\":1,\"of\":3,\"t\":1791171844517}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "2",
              "data": "{\"n\":2,\"of\":3,\"t\":1791171844517}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "3",
              "data": "{\"n\":3,\"of\":3,\"t\":1791171844517}",
              "data_bytes": 32,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "3",
          "last_event_id_buffer_at_eof": "3",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "cr": {
          "flavor": "cr",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 188,
          "raw_sha256": "f356323a7b6ac93e3c6a816bc3cf743cf46ec1a90bc4108dc69c21b2856e5de8",
          "events": [
            {
              "type": "tick",
              "id": "1",
              "data": "{\"n\":1,\"of\":3,\"t\":1791171845722}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "2",
              "data": "{\"n\":2,\"of\":3,\"t\":1791171845722}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "3",
              "data": "{\"n\":3,\"of\":3,\"t\":1791171845722}",
              "data_bytes": 32,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "3",
          "last_event_id_buffer_at_eof": "3",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "no-space": {
          "flavor": "no-space",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 75,
          "raw_sha256": "e35f7346d55abc826290d56c46ad662ba890db98533d95a6d3a4d732beba605f",
          "events": [
            {
              "type": "message",
              "id": "1",
              "data": "foo",
              "data_bytes": 3,
              "data_sha256": null
            },
            {
              "type": "message",
              "id": "2",
              "data": " foo",
              "data_bytes": 4,
              "data_sha256": null
            },
            {
              "type": "message",
              "id": "3",
              "data": "",
              "data_bytes": 0,
              "data_sha256": null
            },
            {
              "type": "message",
              "id": "4",
              "data": "",
              "data_bytes": 0,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "4",
          "last_event_id_buffer_at_eof": "4",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "multiline": {
          "flavor": "multiline",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 122,
          "raw_sha256": "f6f3f2f1029cc1bdaf91057656aebd94d3f424ba5bb5d28655b1293dcc79e49f",
          "events": [
            {
              "type": "message",
              "id": "1",
              "data": "line one\nline two\nline three",
              "data_bytes": 28,
              "data_sha256": null
            },
            {
              "type": "message",
              "id": "2",
              "data": "key: value ",
              "data_bytes": 11,
              "data_sha256": null
            },
            {
              "type": "message",
              "id": "3",
              "data": "a\n\nb",
              "data_bytes": 4,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "3",
          "last_event_id_buffer_at_eof": "3",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "comments": {
          "flavor": "comments",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 159,
          "raw_sha256": "3d349748c5457b2498a863e74d3aa398ca8c791078a5bd1c0d9c80eb3a9ed012",
          "events": [
            {
              "type": "message",
              "id": "1",
              "data": "{\"n\":1}",
              "data_bytes": 7,
              "data_sha256": null
            },
            {
              "type": "message",
              "id": "2",
              "data": "{\"n\":2}",
              "data_bytes": 7,
              "data_sha256": null
            },
            {
              "type": "message",
              "id": "3",
              "data": "{\"n\":3}",
              "data_bytes": 7,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "3",
          "last_event_id_buffer_at_eof": "3",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "split-utf8": {
          "flavor": "split-utf8",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 55,
          "raw_sha256": "d2c78e5078f1b7e3137be2f13c023174d440c7597082d9721721399c5e6dd8a7",
          "events": [
            {
              "type": "message",
              "id": "1",
              "data": "🐍 ok",
              "data_bytes": 7,
              "data_sha256": null
            },
            {
              "type": "message",
              "id": "2",
              "data": "€ ok",
              "data_bytes": 6,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "2",
          "last_event_id_buffer_at_eof": "2",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "wrong-type": {
          "flavor": "wrong-type",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 188,
          "raw_sha256": "15f9a166a7a0f5af087e4910dd7a50f7f8063ab6941c7a7b86adcbada0507c59",
          "events": [
            {
              "type": "tick",
              "id": "1",
              "data": "{\"n\":1,\"of\":3,\"t\":1791171852374}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "2",
              "data": "{\"n\":2,\"of\":3,\"t\":1791171852374}",
              "data_bytes": 32,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "3",
              "data": "{\"n\":3,\"of\":3,\"t\":1791171852374}",
              "data_bytes": 32,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "3",
          "last_event_id_buffer_at_eof": "3",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "error-event": {
          "flavor": "error-event",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 187,
          "raw_sha256": "18d2c500935956c6bc54782c918bed23587c993e0c123fa7e1bc6c755cd814f8",
          "events": [
            {
              "type": "tick",
              "id": "1",
              "data": "{\"n\":1}",
              "data_bytes": 7,
              "data_sha256": null
            },
            {
              "type": "error",
              "id": "2",
              "data": "{\"code\":\"upstream_timeout\",\"message\":\"this is a message, not a transport error\"}",
              "data_bytes": 80,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "3",
              "data": "{\"n\":3}",
              "data_bytes": 7,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "3",
          "last_event_id_buffer_at_eof": "3",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "big": {
          "flavor": "big",
          "source": "reference WHATWG parse of the bytes the control received",
          "raw_bytes": 65564,
          "raw_sha256": "44c576fddd3eb10b9aecfee070b38218b3383b0515ec8eb503a57960bb329423",
          "events": [
            {
              "type": "message",
              "id": "1",
              "data": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx…",
              "data_bytes": 65536,
              "data_sha256": "1f8745f0d2d1387ec1af2211a3cf417b2e9e885e853472649c1d979d0e9370e3"
            }
          ],
          "last_event_id_at_eof": "1",
          "last_event_id_buffer_at_eof": "1",
          "retry_ms": 30000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 1
        },
        "resume": {
          "flavor": "resume",
          "source": "reference WHATWG parse of the bytes the control received; connections 2 and 3 (ids 4–6, then 204) from the documented sequence of /sse/resume, which curl does not reconnect to witness",
          "raw_bytes": 112,
          "raw_sha256": "0fc7a0e0298384bc9bf62e36c7aec84ae1c9b7613f71b5711e9c626f05a0e47e",
          "events": [
            {
              "type": "tick",
              "id": "1",
              "data": "{\"n\":1}",
              "data_bytes": 7,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "2",
              "data": "{\"n\":2}",
              "data_bytes": 7,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "3",
              "data": "{\"n\":3}",
              "data_bytes": 7,
              "data_sha256": null
            }
          ],
          "last_event_id_at_eof": "3",
          "last_event_id_buffer_at_eof": "3",
          "retry_ms": 1000,
          "discarded_pending_data": false,
          "curl_exit": 0,
          "connections_expected": 3,
          "events_later_connections": [
            {
              "type": "tick",
              "id": "4",
              "data": "{\"n\":4}",
              "data_bytes": 7,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "5",
              "data": "{\"n\":5}",
              "data_bytes": 7,
              "data_sha256": null
            },
            {
              "type": "tick",
              "id": "6",
              "data": "{\"n\":6}",
              "data_bytes": 7,
              "data_sha256": null
            }
          ]
        }
      },
      "disagreement_by_flavor": {
        "drop": {
          "distinct_answers_within_class": {
            "eventsource": 2,
            "one-shot": 3
          },
          "answers": {
            "eventsource: 1 events [tick#1:\"{\\\"n\\\":1}\"] | ended reconnecting": [
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "eventsource: 1 events [tick#1:\"{\\\"n\\\":1}\"] | ended error": [
              "aiohttp-sse-client"
            ],
            "one-shot: 1 events [tick#1:\"{\\\"n\\\":1}\"] | ended error": [
              "httpx-sse"
            ],
            "one-shot: 2 events [tick#1:\"{\\\"n\\\":1}\" message#2:\"\"] | ended error": [
              "r3labs-sse"
            ],
            "one-shot: 0 events [] | ended error": [
              "sseclient-py"
            ]
          }
        },
        "big": {
          "distinct_answers_within_class": {
            "eventsource": 2,
            "one-shot": 2
          },
          "answers": {
            "eventsource: 1 events [message#1:<65536b 1f8745f0>] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "node-eventsource"
            ],
            "one-shot: 1 events [message#1:<65536b 1f8745f0>] | ended clean": [
              "httpx-sse",
              "sseclient-py"
            ],
            "eventsource: 0 events [] | ended reconnecting": [
              "go-sse"
            ],
            "one-shot: 0 events [] | ended error": [
              "r3labs-sse"
            ]
          }
        },
        "comments": {
          "distinct_answers_within_class": {
            "eventsource": 2,
            "one-shot": 2
          },
          "answers": {
            "eventsource: 3 events [message#1:\"{\\\"n\\\":1}\" message#2:\"{\\\"n\\\":2}\" message#3:\"{\\\"n\\\":3}\"] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "node-eventsource"
            ],
            "one-shot: 3 events [message#1:\"{\\\"n\\\":1}\" message#2:\"{\\\"n\\\":2}\" message#3:\"{\\\"n\\\":3}\"] | ended clean": [
              "httpx-sse",
              "sseclient-py"
            ],
            "eventsource: 3 events [message#1:\"{\\\"n\\\":1}\" message#2:\"{\\\"n\\\":2}\" message#3:\"{\\\"n\\\":3}\"] | ended clean": [
              "go-sse"
            ],
            "one-shot: 4 events [message#1:\"{\\\"n\\\":1}\" message#2:\"{\\\"n\\\":2}\" message#3:\"{\\\"n\\\":3}\" message#3:\"\"] | ended clean": [
              "r3labs-sse"
            ]
          }
        },
        "cr": {
          "distinct_answers_within_class": {
            "eventsource": 2,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 3 events [tick#1:\"{\\\"n\\\":1,\\\"of\\\":3}\" tick#2:\"{\\\"n\\\":2,\\\"of\\\":3}\" tick#3:\"{\\\"n\\\":3,\\\"of\\\":3}\"] | ended reconnecting": [
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 3 events [tick#1:\"{\\\"n\\\":1,\\\"of\\\":3}\" tick#2:\"{\\\"n\\\":2,\\\"of\\\":3}\" tick#3:\"{\\\"n\\\":3,\\\"of\\\":3}\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ],
            "eventsource: 0 events [] | ended reconnecting": [
              "aiohttp-sse-client"
            ]
          }
        },
        "cut": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 2
          },
          "answers": {
            "eventsource: 1 events [message#1:\"complete\"] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 2 events [message#1:\"complete\" message#2:\"{\\\"partial\\\":tr\"] | ended clean": [
              "r3labs-sse",
              "sseclient-py"
            ],
            "one-shot: 1 events [message#1:\"complete\"] | ended clean": [
              "httpx-sse"
            ]
          }
        },
        "no-space": {
          "distinct_answers_within_class": {
            "eventsource": 2,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 4 events [message#1:\"foo\" message#2:\" foo\" message#3:\"\" message#4:\"\"] | ended reconnecting": [
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 4 events [message#1:\"foo\" message#2:\" foo\" message#3:\"\" message#4:\"\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ],
            "eventsource: 4 events [message#1:\"foo\" message#2:\"foo\" message#3:\"\" message#4:\"\"] | ended reconnecting": [
              "aiohttp-sse-client"
            ]
          }
        },
        "wrong-type": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 2
          },
          "answers": {
            "eventsource: 0 events [] | ended error": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 3 events [tick#1:\"{\\\"n\\\":1,\\\"of\\\":3}\" tick#2:\"{\\\"n\\\":2,\\\"of\\\":3}\" tick#3:\"{\\\"n\\\":3,\\\"of\\\":3}\"] | ended clean": [
              "r3labs-sse",
              "sseclient-py"
            ],
            "one-shot: 0 events [] | ended error": [
              "httpx-sse"
            ]
          }
        },
        "crlf": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 3 events [tick#1:\"{\\\"n\\\":1,\\\"of\\\":3}\" tick#2:\"{\\\"n\\\":2,\\\"of\\\":3}\" tick#3:\"{\\\"n\\\":3,\\\"of\\\":3}\"] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 3 events [tick#1:\"{\\\"n\\\":1,\\\"of\\\":3}\" tick#2:\"{\\\"n\\\":2,\\\"of\\\":3}\" tick#3:\"{\\\"n\\\":3,\\\"of\\\":3}\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ]
          }
        },
        "error-event": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 3 events [tick#1:\"{\\\"n\\\":1}\" error#2:\"{\\\"code\\\":\\\"upstream_timeout\\\",\\\"message\\\":\\\"this is a message, not a transport error\\\"}\" tick#3:\"{\\\"n\\\":3}\"] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 3 events [tick#1:\"{\\\"n\\\":1}\" error#2:\"{\\\"code\\\":\\\"upstream_timeout\\\",\\\"message\\\":\\\"this is a message, not a transport error\\\"}\" tick#3:\"{\\\"n\\\":3}\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ]
          }
        },
        "multiline": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 3 events [message#1:\"line one\\nline two\\nline three\" message#2:\"key: value \" message#3:\"a\\n\\nb\"] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 3 events [message#1:\"line one\\nline two\\nline three\" message#2:\"key: value \" message#3:\"a\\n\\nb\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ]
          }
        },
        "ok": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 5 events [tick#1:\"{\\\"n\\\":1,\\\"of\\\":5}\" tick#2:\"{\\\"n\\\":2,\\\"of\\\":5}\" tick#3:\"{\\\"n\\\":3,\\\"of\\\":5}\" tick#4:\"{\\\"n\\\":4,\\\"of\\\":5}\" tick#5:\"{\\\"n\\\":5,\\\"of\\\":5}\"] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 5 events [tick#1:\"{\\\"n\\\":1,\\\"of\\\":5}\" tick#2:\"{\\\"n\\\":2,\\\"of\\\":5}\" tick#3:\"{\\\"n\\\":3,\\\"of\\\":5}\" tick#4:\"{\\\"n\\\":4,\\\"of\\\":5}\" tick#5:\"{\\\"n\\\":5,\\\"of\\\":5}\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ]
          }
        },
        "resume": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 6 events [tick#1:\"{\\\"n\\\":1}\" tick#2:\"{\\\"n\\\":2}\" tick#3:\"{\\\"n\\\":3}\" tick#4:\"{\\\"n\\\":4}\" tick#5:\"{\\\"n\\\":5}\" tick#6:\"{\\\"n\\\":6}\"] | ended stopped": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 3 events [tick#1:\"{\\\"n\\\":1}\" tick#2:\"{\\\"n\\\":2}\" tick#3:\"{\\\"n\\\":3}\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ]
          }
        },
        "split-utf8": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 2 events [message#1:\"🐍 ok\" message#2:\"€ ok\"] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 2 events [message#1:\"🐍 ok\" message#2:\"€ ok\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ]
          }
        },
        "stall": {
          "distinct_answers_within_class": {
            "eventsource": 1,
            "one-shot": 1
          },
          "answers": {
            "eventsource: 1 events [tick#1:\"{\\\"n\\\":1}\"] | ended reconnecting": [
              "aiohttp-sse-client",
              "eventsource-npm",
              "go-sse",
              "node-eventsource"
            ],
            "one-shot: 1 events [tick#1:\"{\\\"n\\\":1}\"] | ended clean": [
              "httpx-sse",
              "r3labs-sse",
              "sseclient-py"
            ]
          }
        }
      },
      "disagreement_note": "distinct_answers_within_class is, per flavor and per client class, the number of distinct answers (the events delivered, with the server clock \"t\" removed and the retry-only opening events left out, plus how the row ended) among client rows; answers lists them, prefixed by class. Control rows are excluded. The two classes are never compared with each other: an EventSource-interface library ending \"reconnecting\" and a one-shot library ending \"clean\" after the same events is the same result in two idioms.",
      "findings": [
        "Two classes of library were measured and the expected ending differs by class: aiohttp-sse-client, eventsource (npm), go-sse, Node built-in EventSource (undici) implement the EventSource interface (4), which reconnects after any server close and so reports a clean end as an error event in its reconnecting state; httpx-sse, r3labs/sse, sseclient-py iterate one response and return (3). 88 of 98 rows delivered what their class is expected to and ended as expected for it: the reference parse of the control's bytes, except that the EventSource interface delivers nothing on wrong-type and, on resume, also the events 4-6 of the documented later connections (a one-shot library on wrong-type may deliver either).",
        "ok (the control stream, five events): all 7 libraries delivered the five events as the reference parse has them; endings: ended reconnecting (aiohttp-sse-client, eventsource (npm), go-sse, Node built-in EventSource (undici)); ended clean (httpx-sse, r3labs/sse, sseclient-py).",
        "cut (a complete event, then \"data: {\\\"partial\\\":tr\" and EOF with no blank line; the spec discards it): aiohttp-sse-client, eventsource (npm), go-sse, httpx-sse, Node built-in EventSource (undici) delivered one event; r3labs/sse, sseclient-py delivered the unterminated partial as an event. The spec updates the Last-Event-ID string only when a blank line is processed, so after the unterminated \"id: 2\" a reconnect still carries 1; the last event id each row records: exposes no last event id (eventsource (npm), Node built-in EventSource (undici), sseclient-py); id of the last delivered event 1 (go-sse, httpx-sse); last event id 1 (aiohttp-sse-client); last event id 2 (r3labs/sse). For go-sse, httpx-sse that value is the id of the last event the library delivered, not library state the harness could read, so it cannot show what a reconnect would carry; aiohttp-sse-client, r3labs/sse were read from the library's own state.",
        "drop (the connection reset mid-event): all 7 libraries reported it as an error or entered their reconnecting state; sseclient-py delivered none of the complete event that arrived before the reset; r3labs/sse delivered the half-written second event as an event of its own. stall (one event, then ten seconds of silence, then a close): 1 event, then ended reconnecting after 10 s (aiohttp-sse-client, eventsource (npm), go-sse, Node built-in EventSource (undici)); 1 event, then ended clean after 10 s (httpx-sse, r3labs/sse, sseclient-py).",
        "crlf: every library delivered the three events with clean values. cr (bare CR line endings, spec-legal): eventsource (npm), go-sse, httpx-sse, Node built-in EventSource (undici), r3labs/sse, sseclient-py parsed it as the spec says; aiohttp-sse-client delivered nothing at all.",
        "no-space (\"data:foo\" is \"foo\", \"data:  foo\" is \" foo\", \"data: \" and \"data:\" are \"\" and still fire): as the spec strips (eventsource (npm), go-sse, httpx-sse, Node built-in EventSource (undici), r3labs/sse, sseclient-py); values \"foo\" \"foo\" \"\" \"\" (aiohttp-sse-client).",
        "multiline (three data lines joined with LF, a colon inside a value, an empty data line in the middle): every library delivered the three values as the reference parse has them. comments (a BOM, comment lines, an unknown field, a line with no colon): three events, BOM and comments ignored (aiohttp-sse-client, eventsource (npm), httpx-sse, Node built-in EventSource (undici), sseclient-py); 3 events, ended clean, expected reconnecting for an eventsource client (go-sse); 4 events, delivered 4 events, the reference has 3 (extra: message#3:\"\") (r3labs/sse). go-sse delivered the three events and then ended clean on one connection without scheduling a reconnect, though its Connection.Connect reconnects after the stream ends on every other flavor here; comments is also the only flavor whose last line is a comment, and no row isolates which of its features produced this.",
        "split-utf8 (a 4-byte and a 3-byte character each split across two chunks): all 7 libraries reassembled both characters.",
        "wrong-type (a valid stream served as text/plain; the EventSource interface must fail the connection): aiohttp-sse-client, eventsource (npm), go-sse, httpx-sse, Node built-in EventSource (undici) refused it and delivered nothing; r3labs/sse, sseclient-py delivered the three events regardless. A one-shot library is under no specification obligation to check the type; the rows say which did.",
        "error-event (an event whose name is \"error\", between two ticks): aiohttp-sse-client, eventsource (npm), go-sse, httpx-sse, Node built-in EventSource (undici), r3labs/sse, sseclient-py delivered it as a named event — and for eventsource (npm), Node built-in EventSource (undici) it arrived on the same error listener as a transport failure would, so a caller must look for the event's data to tell them apart.",
        "big (one 65,536-byte data line): the 65,536-byte line intact (aiohttp-sse-client, eventsource (npm), httpx-sse, Node built-in EventSource (undici), sseclient-py); nothing of it (request failed: connection to server lost: bufio.Scanner: token too long) (go-sse); nothing of it (bufio.Scanner: token too long) (r3labs/sse). Both Go libraries read lines through a bufio.Scanner whose default token limit is 64 KiB, and a 65,536-byte data line plus its field name is longer than that.",
        "The stream's opening retry block (\"retry: 30000\"; \"retry: 1000\" on resume) dispatches no event in the specification (it has no data), and go-sse (on 13 of 14 flavors), httpx-sse (on 12 of 14 flavors), r3labs/sse (on 13 of 14 flavors) delivered an empty event for it; those events are recorded on the rows (preamble_events) and excluded from the comparison so they do not hide anything else. An empty event at the very end of a stream is not the opening block and is kept in the comparison.",
        "resume (ids 1–3, then 4–6 to a reconnect carrying Last-Event-ID: 3, then a 204): aiohttp-sse-client, eventsource (npm), go-sse, Node built-in EventSource (undici) ran the whole sequence — three connections, six events, stopped at the 204; per library: 3 connections, 6 events, ended stopped, Last-Event-ID sent: 3 then 6 (aiohttp-sse-client, eventsource (npm), go-sse, Node built-in EventSource (undici)); one connection, 3 events, no reconnection by design (httpx-sse, r3labs/sse, sseclient-py).",
        "0 of 98 rows hit the harness's 30 s cap and 0 failed to land. Every other row is a stream this server sent, read back from the wire with its x-badhttp-version on every connection."
      ],
      "freshness": "A dated capture, not a live measurement: taken 2026-10-05T03:43:48Z against badhttp version 0.21.0, with the library versions on each row. Library behaviour changes between releases and this table does not update itself. Re-run it from scripts/sse-witness/ in the source and the numbers move; the date on every row is how you know whether to trust it.",
      "reproduce": "Point the row's library at the row's url with a fresh instance and collect every event it delivers (type, id, data) and how it signals the end; compare with the row's events and end. For the reference, capture the bytes with curl -sS -N --http1.1 and parse them by WHATWG HTML §9.2.6 (scripts/witness-parse-sse.mjs carries the parser). Pace the calls against the zone limit of 100 per 10 s, and treat a first response without x-badhttp-version as no observation."
    },
    "range": {
      "what": "What 10 real resuming download clients did with the fourteen /range flavors, one row per observation, plus one raw-wire control row per flavor (curl). Each cell holds the first 400 bytes of the 1000-byte document in a fresh working directory and tells the client to resume it from GET /range/{flavor}; one extra cell per client downloads from nothing at always-206. Each row lists every request the client sent, what was left on disk and how the client exited. The oracle is the reference placement of the bytes the control received, checked against the true document.",
      "rows": 166,
      "flavors": 14,
      "cells_per_client": 15,
      "observed": "2026-10-12T05:39:32.005Z",
      "badhttp_version_observed": "0.23.0",
      "worker_version": "57282a16",
      "clients": [
        {
          "id": "curl-raw",
          "name": "curl",
          "role": "control",
          "class": "raw",
          "version": "curl 8.7.1 (x86_64-apple-darwin26.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.69.0",
          "library": null,
          "platform": "Darwin arm64",
          "invocation": "curl -sS --http1.1 [-H 'Range: bytes=400-'] -o raw.bin <url> (the Range header on resume-raw rows only)",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "curl",
          "name": "curl",
          "role": "client",
          "class": "resumer",
          "version": "curl 8.7.1 (x86_64-apple-darwin26.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.69.0",
          "library": null,
          "platform": "Darwin arm64",
          "invocation": "curl --http1.1 -sS -C - -o out.txt <url>",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "go-getter",
          "name": "go-getter",
          "role": "client",
          "class": "resumer",
          "version": "v1.8.10",
          "library": "go-getter",
          "platform": "Darwin arm64",
          "invocation": "gotool getter <url> out.txt",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "grab",
          "name": "grab",
          "role": "client",
          "class": "resumer",
          "version": "v3.0.1",
          "library": "grab",
          "platform": "Darwin arm64",
          "invocation": "gotool grab <url> out.txt",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "httpie",
          "name": "HTTPie",
          "role": "client",
          "class": "resumer",
          "version": "3.2.4",
          "library": null,
          "platform": "Darwin arm64",
          "invocation": "http --ignore-stdin --download --continue --output out.txt <url>",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "huggingface_hub",
          "name": "huggingface_hub",
          "role": "client",
          "class": "resumer",
          "version": "2.2.0",
          "library": "huggingface_hub",
          "platform": "Darwin arm64",
          "invocation": "python -I hf.py <url> out.txt",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "node-downloader-helper",
          "name": "node-downloader-helper",
          "role": "client",
          "class": "resumer",
          "version": "2.1.11",
          "library": "node-downloader-helper",
          "platform": "Darwin arm64",
          "invocation": "node ndh.cjs <url> .",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "nugget",
          "name": "nugget",
          "role": "client",
          "class": "resumer",
          "version": "2.2.0",
          "library": null,
          "platform": "Darwin arm64",
          "invocation": "node nugget/bin.js -q -c -o out.txt -d . <url>",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "wget",
          "name": "GNU Wget",
          "role": "client",
          "class": "resumer",
          "version": "GNU Wget 1.25.0 built on darwin27.0.0.",
          "library": null,
          "platform": "Darwin arm64",
          "invocation": "wget -c -t 1 -O out.txt <url>",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "wget2",
          "name": "GNU Wget2",
          "role": "client",
          "class": "resumer",
          "version": "GNU Wget2 2.3.0 - multithreaded metalink/file/website downloader",
          "library": null,
          "platform": "Darwin arm64",
          "invocation": "wget2 -c -t 1 --no-http2 -O out.txt <url>",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "yt-dlp",
          "name": "yt-dlp",
          "role": "client",
          "class": "resumer",
          "version": "2026.08.19",
          "library": null,
          "platform": "Darwin arm64",
          "invocation": "yt-dlp --continue --retries 0 --fragment-retries 0 --no-progress --no-warnings --no-cache-dir --ignore-config -o out.txt <url>",
          "requests_recorded_by": "scripts/range-witness/recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        }
      ],
      "fields": [
        "id",
        "corpus_id",
        "family",
        "flavor",
        "url",
        "client",
        "observed",
        "badhttp_version_observed",
        "status",
        "reported_error",
        "outcome",
        "license",
        "class",
        "scenario",
        "partial_bytes",
        "partial_sha256",
        "exchanges",
        "exit",
        "file",
        "end",
        "wall_ms",
        "attempts",
        "shape",
        "transfer",
        "missing_bytes",
        "outcome_vs_reference",
        "diff",
        "reference",
        "control",
        "oracle"
      ],
      "class_legend": {
        "resumer": "A real download client or library told to resume a partial file from the row's url. The harness never sends a Range header itself: the client decides, from the size of the partial, what to ask for and what to do with the answer. Every request it sent went through a local recording proxy, so exchanges lists them all.",
        "raw": "The control: curl sending exactly Range: bytes=400-, with no resume logic and no file handling at all. Its control.raw_sha256 is the hash of the bytes the reference placement was derived from; on the document row it fetched the whole document with no Range, which is the true document every file is compared with."
      },
      "outcome_legend": {
        "complete": "The file on disk is the whole 1,000-byte document, byte for byte. transfer says how, judged by the deciding exchange (the last request other than HEAD): resumed (it carried a Range and returned fewer bytes than the document, so the partial was kept), restarted (it returned the whole document, so the partial was thrown away), restarted-after-range (an earlier non-HEAD request asked for a range and got fewer bytes than the document, then a full fetch followed), or fresh (the download from nothing). When the deciding exchange has no Range and is short, the bytes summed across the exchanges decide.",
        "refused": "The file is the untouched partial (for the fresh cell, absent or empty) and the client reported an error: it stopped with an error and nothing was written.",
        "unchanged": "The file is the untouched partial, byte for byte, and the client reported no error: it exited without complaint and wrote nothing.",
        "corrupt": "The file differs from the document and is not the untouched partial. shape says how, computed from the self-describing lines.",
        "incomplete-reported": "The file is a prefix of the document shorter than 1,000 bytes and the client reported an error: it stopped and said so.",
        "harness-timeout": "The harness's 30 s per-row cap fired; the row describes the file as it stood and the exchanges made so far.",
        "request-failed": "No observation: the rate limit never cleared, or the recorder saw no response carrying x-badhttp-version. Such a row has no exchanges.",
        "control": "A curl control row: bytes, not a file. It is the source of the reference placement (resume-raw, fresh-raw) or of the true document (document)."
      },
      "shape_legend": {
        "duplicated-prefix": "The document's head appears again after byte 400: the partial was kept and the whole document was appended to it.",
        "shifted": "Lines whose embedded offset is one away from the position they sit at: bytes a+1..b+1 were written where a..b belong.",
        "short": "A prefix of the document shorter than 1,000 bytes with the client reporting success; missing_bytes says how many bytes are absent.",
        "mixed-generation": "Lines from more than one generation (the if-range-ignored flavor stamps a generation into every line).",
        "suffix-misplaced": "The duplicated-prefix pattern on the suffix-as-prefix flavor, where the head of the document is served under a Content-Range that names its tail.",
        "body-only": "The file is the deciding response's body alone: the partial was discarded and the file holds only what the server sent in the response.",
        "other": "A file that differs from the document in a way none of the other shapes names; first_bad_offset on the row says where, and diff says what."
      },
      "transfer_legend": {
        "resumed": "Kept the partial: the deciding exchange (the last request other than HEAD) carried a Range and returned fewer bytes than the document. A leading HEAD, or a GET with no Range that comes first, is a probe and is not counted. When the deciding exchange has no Range and is short, the bytes summed across the exchanges decide.",
        "restarted": "Threw the partial away and wrote the whole representation from a single response.",
        "restarted-after-range": "Asked for a range first, then fetched the whole document.",
        "fresh": "The fresh cell: no partial existed, the client downloaded from nothing."
      },
      "reference_legend": {
        "as-reference": "The file is what applying the reference placement (where RFC 9110 puts the bytes the control received) to the true partial yields. On a flavor whose reference placement does not yield the document, this includes a corrupt file.",
        "differs": "The file is not what the reference placement yields; diff names the first difference.",
        "undefined": "No reference placement exists: the control's response says nothing about where its bytes belong (a 206 without Content-Range, a 416 at an offset inside the document)."
      },
      "reading_this": "Read outcome as a description of the file on disk and the client's exit after one resume, never as a verdict on the client. RFC 9110 defines what a 206 means (its Content-Range says where its body belongs in the representation), what a 200 carries (the whole representation) and what a 416 says (the complete length); it defines no algorithm for a client that resumes. A client that starts over on a 200 and one that stops with an error are both doing something the specification allows, so refused and complete are different answers, not a better and a worse one. Each row is also described relative to its reference placement: where RFC 9110 puts the bytes the curl control received for Range: bytes=400-, applied to the true 400-byte partial. Where that placement itself does not yield the document (off-by-one, shifted, 200-content-range, if-range-ignored) the lie is invisible to anyone not reading the body, and the document is self-describing — 64-byte lines that each carry their own offset — so the file convicts itself: shape and first_bad_offset come from those lines. outcome_vs_reference says whether the file matches the placement; it is null on a harness-timeout row, whose file is a mid-transfer snapshot that was never compared. The exchanges array is every request the client sent and every response it got, recorded between the client and this service; the clients spoke HTTP/1.1 to a local proxy, so TLS and HTTP/2 behaviour is out of scope. No client here is scored or ranked.",
      "outcome_counts": {
        "control": 16,
        "complete": 84,
        "refused": 17,
        "corrupt": 42,
        "incomplete-reported": 2,
        "harness-timeout": 2,
        "unchanged": 3
      },
      "shape_counts": {
        "duplicated-prefix": 12,
        "short": 7,
        "shifted": 9,
        "body-only": 4,
        "mixed-generation": 10
      },
      "transfer_counts": {
        "resumed": 59,
        "restarted": 15,
        "fresh": 10
      },
      "class_counts": {
        "raw": 1,
        "resumer": 10
      },
      "reference": {
        "ok": {
          "flavor": "ok",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 400-999",
          "status": 206,
          "content_range": "bytes 400-999/1000",
          "content_length": "600",
          "body_bytes": 600,
          "raw_sha256": "16c7aca9b0cb93b5579f09cde65b03c8da0c1d9ae8ab8b6766c0ea9a5aab6d5f",
          "body_matches_content_range": true,
          "yields_document": true,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 400-999/1000 says its body is bytes 400 through 999 of the representation, so the body is placed at offset 400 of the file.",
          "reason": null
        },
        "ignore": {
          "flavor": "ignore",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "whole representation",
          "status": 200,
          "content_range": null,
          "content_length": "1000",
          "body_bytes": 1000,
          "raw_sha256": "045132f43db2e244fad1816c994ed1f2f7e47223c998685141248d142a542dee",
          "body_matches_content_range": null,
          "yields_document": true,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.1: a 200 carries the whole representation, so its body replaces the partial file, and a Content-Range on a 200 has no defined meaning (§14.4).",
          "reason": null
        },
        "advertise-only": {
          "flavor": "advertise-only",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "whole representation",
          "status": 200,
          "content_range": null,
          "content_length": "1000",
          "body_bytes": 1000,
          "raw_sha256": "045132f43db2e244fad1816c994ed1f2f7e47223c998685141248d142a542dee",
          "body_matches_content_range": null,
          "yields_document": true,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.1: a 200 carries the whole representation, so its body replaces the partial file, and a Content-Range on a 200 has no defined meaning (§14.4).",
          "reason": null
        },
        "off-by-one": {
          "flavor": "off-by-one",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 400-999",
          "status": 206,
          "content_range": "bytes 400-999/1000",
          "content_length": "599",
          "body_bytes": 599,
          "raw_sha256": "f488b26ffad053c7920a91d4d14ffc1c8593f406afbf69d5207c65c33c5785fe",
          "body_matches_content_range": false,
          "yields_document": false,
          "resulting_bytes": 999,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 400-999/1000 says its body is bytes 400 through 999 of the representation, so the body is placed at offset 400 of the file.",
          "reason": null
        },
        "shifted": {
          "flavor": "shifted",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 400-999",
          "status": 206,
          "content_range": "bytes 400-999/1000",
          "content_length": "599",
          "body_bytes": 599,
          "raw_sha256": "51f3fcb760452692119b037a99b4ad79a79020510d6d8da735481aafcfe94022",
          "body_matches_content_range": false,
          "yields_document": false,
          "resulting_bytes": 999,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 400-999/1000 says its body is bytes 400 through 999 of the representation, so the body is placed at offset 400 of the file.",
          "reason": null
        },
        "suffix-as-prefix": {
          "flavor": "suffix-as-prefix",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 400-999",
          "status": 206,
          "content_range": "bytes 400-999/1000",
          "content_length": "600",
          "body_bytes": 600,
          "raw_sha256": "16c7aca9b0cb93b5579f09cde65b03c8da0c1d9ae8ab8b6766c0ea9a5aab6d5f",
          "body_matches_content_range": true,
          "yields_document": true,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 400-999/1000 says its body is bytes 400 through 999 of the representation, so the body is placed at offset 400 of the file.",
          "reason": null
        },
        "from-zero": {
          "flavor": "from-zero",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 0-999",
          "status": 206,
          "content_range": "bytes 0-999/1000",
          "content_length": "1000",
          "body_bytes": 1000,
          "raw_sha256": "045132f43db2e244fad1816c994ed1f2f7e47223c998685141248d142a542dee",
          "body_matches_content_range": true,
          "yields_document": true,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 0-999/1000 says its body is bytes 0 through 999 of the representation, so the body is placed at offset 0 of the file.",
          "reason": null
        },
        "wrong-total": {
          "flavor": "wrong-total",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 400-999",
          "status": 206,
          "content_range": "bytes 400-999/2000",
          "content_length": "600",
          "body_bytes": 600,
          "raw_sha256": "16c7aca9b0cb93b5579f09cde65b03c8da0c1d9ae8ab8b6766c0ea9a5aab6d5f",
          "body_matches_content_range": true,
          "yields_document": true,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 400-999/2000 says its body is bytes 400 through 999 of the representation, so the body is placed at offset 400 of the file.",
          "reason": null
        },
        "no-content-range": {
          "flavor": "no-content-range",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "undefined",
          "status": 206,
          "content_range": null,
          "content_length": "600",
          "body_bytes": 600,
          "raw_sha256": "16c7aca9b0cb93b5579f09cde65b03c8da0c1d9ae8ab8b6766c0ea9a5aab6d5f",
          "body_matches_content_range": null,
          "yields_document": null,
          "resulting_bytes": null,
          "rule": "RFC 9110 §15.3.7.1: a single-range 206 must carry Content-Range, and without it the offset of the body is not defined.",
          "reason": "a 206 without Content-Range says nothing about where its body belongs"
        },
        "always-206": {
          "flavor": "always-206",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 400-999",
          "status": 206,
          "content_range": "bytes 400-999/1000",
          "content_length": "600",
          "body_bytes": 600,
          "raw_sha256": "16c7aca9b0cb93b5579f09cde65b03c8da0c1d9ae8ab8b6766c0ea9a5aab6d5f",
          "body_matches_content_range": true,
          "yields_document": true,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 400-999/1000 says its body is bytes 400 through 999 of the representation, so the body is placed at offset 400 of the file.",
          "reason": null,
          "fresh": {
            "source": "where RFC 9110 places the bytes the control received for a request with no Range, applied to an empty file",
            "placement": "bytes 0-999",
            "status": 206,
            "content_range": "bytes 0-999/1000",
            "content_length": "1000",
            "body_bytes": 1000,
            "raw_sha256": "045132f43db2e244fad1816c994ed1f2f7e47223c998685141248d142a542dee",
            "body_matches_content_range": true,
            "yields_document": true,
            "resulting_bytes": 1000,
            "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 0-999/1000 says its body is bytes 0 through 999 of the representation, so the body is placed at offset 0 of the file.",
            "reason": null
          }
        },
        "200-content-range": {
          "flavor": "200-content-range",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "whole representation",
          "status": 200,
          "content_range": "bytes 400-999/1000",
          "content_length": "600",
          "body_bytes": 600,
          "raw_sha256": "16c7aca9b0cb93b5579f09cde65b03c8da0c1d9ae8ab8b6766c0ea9a5aab6d5f",
          "body_matches_content_range": null,
          "yields_document": false,
          "resulting_bytes": 600,
          "rule": "RFC 9110 §15.3.1: a 200 carries the whole representation, so its body replaces the partial file, and a Content-Range on a 200 has no defined meaning (§14.4).",
          "reason": null
        },
        "always-416": {
          "flavor": "always-416",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "undefined",
          "status": 416,
          "content_range": "bytes */1000",
          "content_length": "0",
          "body_bytes": 0,
          "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
          "body_matches_content_range": null,
          "yields_document": null,
          "resulting_bytes": null,
          "rule": "RFC 9110 §15.5.17: a 416 says the requested range cannot be satisfied and its Content-Range */t gives the current length t, and it carries no representation bytes, so nothing is placed.",
          "reason": "the 416 names a length of 1000 against a partial of 400 bytes, and places no byte"
        },
        "unknown-total": {
          "flavor": "unknown-total",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 400-999",
          "status": 206,
          "content_range": "bytes 400-999/*",
          "content_length": "600",
          "body_bytes": 600,
          "raw_sha256": "16c7aca9b0cb93b5579f09cde65b03c8da0c1d9ae8ab8b6766c0ea9a5aab6d5f",
          "body_matches_content_range": true,
          "yields_document": true,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 400-999/* says its body is bytes 400 through 999 of the representation, so the body is placed at offset 400 of the file.",
          "reason": null
        },
        "if-range-ignored": {
          "flavor": "if-range-ignored",
          "source": "where RFC 9110 places the bytes the control received for Range: bytes=400-, applied to the first 400 bytes of the control document",
          "placement": "bytes 400-999",
          "status": 206,
          "content_range": "bytes 400-999/1000",
          "content_length": "600",
          "body_bytes": 600,
          "raw_sha256": "d1393ebc784a1e43189182db914dc826191887c5931e64bca2703ff3b9c76ae1",
          "body_matches_content_range": true,
          "yields_document": false,
          "resulting_bytes": 1000,
          "rule": "RFC 9110 §15.3.7 and §14.4: a 206 whose Content-Range is bytes 400-999/1000 says its body is bytes 400 through 999 of the representation, so the body is placed at offset 400 of the file.",
          "reason": null
        }
      },
      "disagreement_by_flavor": {
        "ok": 1,
        "ignore": 3,
        "advertise-only": 3,
        "off-by-one": 3,
        "shifted": 2,
        "suffix-as-prefix": 1,
        "from-zero": 3,
        "wrong-total": 2,
        "no-content-range": 3,
        "always-206": 2,
        "200-content-range": 2,
        "always-416": 2,
        "unknown-total": 2,
        "if-range-ignored": 1,
        "always-206/fresh": 1
      },
      "answers_by_flavor": {
        "ok": [
          {
            "answer": "complete | - | resumed",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget",
              "wget2",
              "yt-dlp"
            ]
          }
        ],
        "ignore": [
          {
            "answer": "refused | - | -",
            "clients": [
              "curl"
            ]
          },
          {
            "answer": "complete | - | restarted",
            "clients": [
              "go-getter",
              "grab",
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "wget",
              "yt-dlp"
            ]
          },
          {
            "answer": "corrupt | duplicated-prefix | -",
            "clients": [
              "nugget",
              "wget2"
            ]
          }
        ],
        "advertise-only": [
          {
            "answer": "refused | - | -",
            "clients": [
              "curl"
            ]
          },
          {
            "answer": "corrupt | duplicated-prefix | -",
            "clients": [
              "go-getter",
              "grab",
              "nugget",
              "wget2"
            ]
          },
          {
            "answer": "complete | - | restarted",
            "clients": [
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "wget",
              "yt-dlp"
            ]
          }
        ],
        "off-by-one": [
          {
            "answer": "corrupt | short | -",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "node-downloader-helper",
              "nugget",
              "wget2",
              "yt-dlp"
            ]
          },
          {
            "answer": "incomplete-reported | - | -",
            "clients": [
              "httpie",
              "huggingface_hub"
            ]
          },
          {
            "answer": "harness-timeout | - | -",
            "clients": [
              "wget"
            ]
          }
        ],
        "shifted": [
          {
            "answer": "corrupt | shifted | -",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget2",
              "yt-dlp"
            ]
          },
          {
            "answer": "harness-timeout | - | -",
            "clients": [
              "wget"
            ]
          }
        ],
        "suffix-as-prefix": [
          {
            "answer": "complete | - | resumed",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget",
              "wget2",
              "yt-dlp"
            ]
          }
        ],
        "from-zero": [
          {
            "answer": "refused | - | -",
            "clients": [
              "curl",
              "httpie",
              "wget"
            ]
          },
          {
            "answer": "corrupt | duplicated-prefix | -",
            "clients": [
              "go-getter",
              "grab",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget2"
            ]
          },
          {
            "answer": "complete | - | restarted",
            "clients": [
              "yt-dlp"
            ]
          }
        ],
        "wrong-total": [
          {
            "answer": "complete | - | resumed",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget",
              "wget2",
              "yt-dlp"
            ]
          },
          {
            "answer": "refused | - | -",
            "clients": [
              "httpie"
            ]
          }
        ],
        "no-content-range": [
          {
            "answer": "refused | - | -",
            "clients": [
              "curl",
              "httpie",
              "wget"
            ]
          },
          {
            "answer": "complete | - | resumed",
            "clients": [
              "go-getter",
              "grab",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget2"
            ]
          },
          {
            "answer": "corrupt | body-only | -",
            "clients": [
              "yt-dlp"
            ]
          }
        ],
        "always-206": [
          {
            "answer": "complete | - | resumed",
            "clients": [
              "curl",
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget",
              "wget2",
              "yt-dlp"
            ]
          },
          {
            "answer": "complete | - | restarted",
            "clients": [
              "go-getter",
              "grab"
            ]
          }
        ],
        "200-content-range": [
          {
            "answer": "complete | - | resumed",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "nugget",
              "wget",
              "wget2",
              "yt-dlp"
            ]
          },
          {
            "answer": "corrupt | body-only | -",
            "clients": [
              "httpie",
              "huggingface_hub",
              "node-downloader-helper"
            ]
          }
        ],
        "always-416": [
          {
            "answer": "unchanged | - | -",
            "clients": [
              "curl",
              "wget",
              "wget2"
            ]
          },
          {
            "answer": "refused | - | -",
            "clients": [
              "go-getter",
              "grab",
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "yt-dlp"
            ]
          }
        ],
        "unknown-total": [
          {
            "answer": "complete | - | resumed",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "httpie",
              "node-downloader-helper",
              "nugget",
              "wget",
              "wget2",
              "yt-dlp"
            ]
          },
          {
            "answer": "refused | - | -",
            "clients": [
              "huggingface_hub"
            ]
          }
        ],
        "if-range-ignored": [
          {
            "answer": "corrupt | mixed-generation | -",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget",
              "wget2",
              "yt-dlp"
            ]
          }
        ],
        "always-206/fresh": [
          {
            "answer": "complete | - | fresh",
            "clients": [
              "curl",
              "go-getter",
              "grab",
              "httpie",
              "huggingface_hub",
              "node-downloader-helper",
              "nugget",
              "wget",
              "wget2",
              "yt-dlp"
            ]
          }
        ]
      },
      "disagreement_note": "disagreement_by_flavor is, per flavor, the number of distinct answers — (outcome, shape, transfer) — among resumer rows; answers_by_flavor lists them with the clients that gave each. The always-206/fresh key is the fresh cell. Control rows are excluded.",
      "findings": [
        "10 resuming download clients (curl, go-getter, grab, HTTPie, huggingface_hub, node-downloader-helper, nugget, GNU Wget, GNU Wget2, yt-dlp) were each given a 400-byte partial of the 1,000-byte document and told to resume it from /range/{flavor} — 14 flavors, plus one fresh download at always-206 — 150 rows in all. By the file and the exit: 84 complete, 42 corrupt, 17 refused, 3 unchanged, 2 incomplete-reported, 2 harness-timeout. Of the 42 corrupt rows, 19 wrote what the reference placement (where RFC 9110 puts the bytes the control received) itself yields and 22 differ from it.",
        "ok (the control got 206 with Content-Range bytes 400-999/1000 and 600 body bytes; reference placement: bytes 400-999, which yields the document): all 10 clients finished the file by keeping the partial.",
        "ignore (the control got 200 with no Content-Range and 1000 body bytes; reference placement: whole representation, which yields the document): go-getter, grab, HTTPie, huggingface_hub, node-downloader-helper, GNU Wget, yt-dlp finished the file by starting over after one response; nugget, GNU Wget2 wrote a file that is not the document (shape duplicated-prefix); curl stopped with an error and left the partial untouched.",
        "advertise-only (the control got 200 with no Content-Range and 1000 body bytes; reference placement: whole representation, which yields the document): HTTPie, huggingface_hub, node-downloader-helper, GNU Wget, yt-dlp finished the file by starting over after one response; go-getter, grab, nugget, GNU Wget2 wrote a file that is not the document (shape duplicated-prefix); curl stopped with an error and left the partial untouched.",
        "off-by-one (the control got 206 with Content-Range bytes 400-999/1000 and 599 body bytes; reference placement: bytes 400-999, which does not yield the document): curl, go-getter, grab, node-downloader-helper, nugget, GNU Wget2, yt-dlp wrote a file that is not the document (shape short); HTTPie, huggingface_hub stopped with an error after a prefix of the document; GNU Wget was still going when the 30 s cap fired.",
        "shifted (the control got 206 with Content-Range bytes 400-999/1000 and 599 body bytes; reference placement: bytes 400-999, which does not yield the document): curl, go-getter, grab, HTTPie, huggingface_hub, node-downloader-helper, nugget, GNU Wget2, yt-dlp wrote a file that is not the document (shape shifted); GNU Wget was still going when the 30 s cap fired.",
        "suffix-as-prefix (the control got 206 with Content-Range bytes 400-999/1000 and 600 body bytes; reference placement: bytes 400-999, which yields the document): all 10 clients finished the file by keeping the partial.",
        "from-zero (the control got 206 with Content-Range bytes 0-999/1000 and 1000 body bytes; reference placement: bytes 0-999, which yields the document): go-getter, grab, huggingface_hub, node-downloader-helper, nugget, GNU Wget2 wrote a file that is not the document (shape duplicated-prefix); curl, HTTPie, GNU Wget stopped with an error and left the partial untouched; yt-dlp finished the file by starting over after one response.",
        "wrong-total (the control got 206 with Content-Range bytes 400-999/2000 and 600 body bytes; reference placement: bytes 400-999, which yields the document): curl, go-getter, grab, huggingface_hub, node-downloader-helper, nugget, GNU Wget, GNU Wget2, yt-dlp finished the file by keeping the partial; HTTPie stopped with an error and left the partial untouched.",
        "no-content-range (the control got 206 with no Content-Range and 600 body bytes; reference placement: none (a 206 without Content-Range says nothing about where its body belongs)): go-getter, grab, huggingface_hub, node-downloader-helper, nugget, GNU Wget2 finished the file by keeping the partial; curl, HTTPie, GNU Wget stopped with an error and left the partial untouched; yt-dlp discarded the partial and kept the response body alone as the file (600 bytes).",
        "always-206 (the control got 206 with Content-Range bytes 400-999/1000 and 600 body bytes; reference placement: bytes 400-999, which yields the document): curl, HTTPie, huggingface_hub, node-downloader-helper, nugget, GNU Wget, GNU Wget2, yt-dlp finished the file by keeping the partial; go-getter, grab finished the file by starting over after one response.",
        "always-206, fresh (no partial, so no Range is sent; the control got 206 with Content-Range bytes 0-999/1000 and 1000 body bytes): all 10 clients wrote the whole document.",
        "200-content-range (the control got 200 with Content-Range bytes 400-999/1000 and 600 body bytes; reference placement: whole representation, which does not yield the document): curl, go-getter, grab, nugget, GNU Wget, GNU Wget2, yt-dlp finished the file by keeping the partial; HTTPie, huggingface_hub, node-downloader-helper discarded the partial and kept the response body alone as the file (600 bytes).",
        "always-416 (the control got 416 with Content-Range bytes */1000 and 0 body bytes; reference placement: none (the 416 names a length of 1000 against a partial of 400 bytes, and places no byte)): go-getter, grab, HTTPie, huggingface_hub, node-downloader-helper, nugget, yt-dlp stopped with an error and left the partial untouched; curl, GNU Wget, GNU Wget2 reported no error and left the partial untouched.",
        "unknown-total (the control got 206 with Content-Range bytes 400-999/* and 600 body bytes; reference placement: bytes 400-999, which yields the document): curl, go-getter, grab, HTTPie, node-downloader-helper, nugget, GNU Wget, GNU Wget2, yt-dlp finished the file by keeping the partial; huggingface_hub stopped with an error and left the partial untouched.",
        "if-range-ignored (the control got 206 with Content-Range bytes 400-999/1000 and 600 body bytes; reference placement: bytes 400-999, which does not yield the document): all 10 clients wrote a file that is not the document (shape mixed-generation).",
        "First request on a resume row: curl, HTTPie, huggingface_hub, GNU Wget, GNU Wget2 opened with a Range GET; go-getter, grab, node-downloader-helper opened with a HEAD; nugget, yt-dlp opened with a GET with no Range. 5 of 10 clients opened at least one resume row with a HEAD or a GET carrying no Range rather than a Range GET (go-getter, grab, node-downloader-helper, nugget, yt-dlp). The harness never sends a Range header itself, so each client chose what to ask for from the size of the partial file.",
        "Validators: none of the 224 requests the clients sent carried If-Range, If-Match, If-None-Match or If-Modified-Since, so no row here puts a stale validator to a server.",
        "curl is the one client that stopped on ignore and advertise-only: lib/http.c accepts the answer to a resume only when its Content-Range begins at the offset curl asked for, and anything else is CURLE_RANGE_ERROR, exit 33, \"HTTP server doesn't seem to support byte ranges. Cannot resume.\" The same rule is why curl completed 200-content-range: the status is 200, but the Content-Range starts at 400, and curl reads the header, not the status.",
        "A 416 whose Content-Range says */1000 against a 400-byte file read as completion to curl, GNU Wget and GNU Wget2: curl's http.c treats a 416 to a resume as \"no error\" and discards the body (exit 0); wget and wget2 print \"The file is already fully retrieved; nothing to do.\" and exit 0. The 400-byte partial stays, and nothing tells the caller the file is 600 bytes short.",
        "grab and go-getter decide whether to resume from the HEAD they send first: only a 200 HEAD carrying Accept-Ranges: bytes makes them send a Range, and neither reads the Content-Range of the answer (grab's client.go carries a \"// TODO: check Content-Range\"). So on ignore, whose HEAD advertises nothing, they sent no Range and rewrote the file; on advertise-only, whose HEAD advertises ranges and whose answer to the Range is a 200, they appended the whole body after the partial; and on always-206, whose HEAD is itself a 206, they sent no Range and took the full body.",
        "GNU Wget2 appends whatever comes back once it has sent a Range: it opens the output for append on a 206 and, with -O, on a 200 as well. nugget opens with flags \"a\" whenever -c is given, whether or not it sent a Range (on ignore, whose first answer advertised no ranges, it sent none), and reads Content-Range only for its progress bar. Both wrote the full 1,000-byte body of a 200 after the 400-byte partial on ignore and advertise-only.",
        "HTTPie, huggingface_hub and node-downloader-helper start over on a 200 whatever its headers say — HTTPie truncates unless the status is 206, huggingface_hub seeks back and truncates when a resume is answered with a 200, node-downloader-helper resets and truncates — and the body they then keep as the whole file is the 600-byte range the Content-Range described. Each trusted the status over the header; curl, wget, wget2, nugget, grab, go-getter and yt-dlp trusted the header, or did not look, and completed.",
        "HTTPie is the one client that stopped on the total: its parse_content_range requires last-byte-pos + 1 to equal the complete length, so \"bytes 400-999/2000\" is a ContentRangeError before a byte is written. The other nine completed the file; of them only huggingface_hub read the total at all, afterwards, in the consistency check described below.",
        "huggingface_hub reads the complete length as int() of whatever follows the slash in Content-Range, so \"bytes 400-999/*\" raises ValueError before anything is written; the other nine took the bytes and completed.",
        "On from-zero, the three clients that compare the Content-Range start with the offset they asked for — curl, HTTPie, GNU Wget — stopped with the partial untouched; yt-dlp compares it too, but on a mismatch it discards the partial and takes the body, which here is the whole document, so it completed; the other six appended 1,000 bytes after 400 without comparing the start with their offset (huggingface_hub then raised OSError when the 1,400-byte file did not match the 1,000-byte total).",
        "On no-content-range the same three stopped (a 206 without Content-Range is a range error to each), yt-dlp discarded the partial and kept the 600-byte body alone (\"Unable to resume\"), and the other six appended — which, this once, is where the bytes belonged.",
        "On off-by-one, 599 bytes arrive under a Content-Range that promises 600: HTTPie and huggingface_hub wrote them and then reported the shortfall (\"Incomplete download: size=1000; downloaded=999\"; \"Consistency check failed: file should be of size 1000 but has size 999\"); GNU Wget takes its expected length from the Content-Range rather than Content-Length, so it waited on the kept-alive connection for a 600th byte the response had already said it would not send, and the harness cap ended it; the other seven exited 0 with a 999-byte file.",
        "All ten clients mixed generations on if-range-ignored and every one exited 0. None sent If-Range or any other validator with its resume — and this flavor ignores If-Range by design, so a validator would not have changed the answer; what the rows show is that no client here reports a document that changed under it.",
        "huggingface_hub completed the file on wrong-total and on no-content-range and still raised OSError: its consistency check after the download compares the file size with the Content-Range total (2000) or, absent a Content-Range, with the Content-Length of the range body (600), so the caller gets an OSError for a file whose 1,000 bytes match the document.",
        "2 of 150 rows hit the harness's 30 s cap (GNU Wget on off-by-one, 1 request made and the file at 999 bytes, GNU Wget on shifted, 1 request made and the file at 999 bytes) and 0 failed to land. Every other row is a response this server sent, read back from the wire with its x-badhttp-version on every exchange."
      ],
      "freshness": "A dated capture, not a live measurement: taken 2026-10-12T05:39:32.005Z against badhttp version 0.23.0, with the client versions on each row. Client behaviour changes between releases and this table does not update itself. Re-run it from scripts/range-witness/ in the source and the numbers move; the date on every row is how you know whether to trust it.",
      "reproduce": "Write the first 400 bytes of GET /range/ok?length=1000 to a fresh file, point the row's client at the row's url with the instruction to resume that file through a recording proxy, and compare the file and the exit with the row. For the reference, fetch the same url with curl -sS --http1.1 -H 'Range: bytes=400-' and place the response as RFC 9110 §15.3.7 says (scripts/witness-parse-range.mjs carries the placement rules). Pace the calls against the zone limit of 100 per 10 s, and treat a response without x-badhttp-version as no observation."
    },
    "etag": {
      "what": "What 14 real HTTP caches and conditional-request clients did with the ten /etag flavors, one row per observation, plus the control rows (curl: a cold request, a textbook revalidation, and on mismatch the revalidation with the tag the 304 carried). Each cell asks the client to fetch GET /etag/{flavor} three times in a row through a fresh cache; the harness never sends a conditional header for a client. Each row lists every request the client sent, attributed to the fetch that caused it, and what the caller received. The oracle is the reference: what RFC 9111 implies a textbook cache would have sent and shown its caller, given what the control's revalidation got.",
      "rows": 161,
      "flavors": 10,
      "cells_per_client": 10,
      "observed": "2026-10-12T19:44:05.856Z",
      "badhttp_version_observed": "0.24.0",
      "worker_version": "ab9e7dbe",
      "clients": [
        {
          "id": "curl-raw",
          "name": "curl",
          "role": "control",
          "class": "raw",
          "version": "curl 8.7.1 (x86_64-apple-darwin26.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.69.0",
          "library": null,
          "runtime": null,
          "platform": "Darwin arm64",
          "invocation": "curl -sS --http1.1 -D headers.txt -o body.bin [-H 'If-None-Match: <the ETag cold-raw received>'] [-H 'If-Modified-Since: <the Last-Modified cold-raw received>'] <url> (conditional headers on revalidate rows only, each only if the cold-raw response carried it; revalidate-adopted-raw sends the ETag the revalidate-raw 304 carried)",
          "configuration": "curl with no cache, no state file and no cookie jar; every request is a single curl process and every conditional header is copied verbatim from the earlier control response",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "cachecontrol",
          "name": "CacheControl",
          "role": "client",
          "class": "library-cache",
          "version": "0.14.4 (requests 2.34.2)",
          "library": "CacheControl (requests)",
          "runtime": "python 3.14.7",
          "platform": "Darwin arm64",
          "invocation": "python -I py-caches.py cachecontrol",
          "configuration": "cachecontrol.CacheControl(requests.Session(), cache=FileCache(<directory in the working directory>)), cache_etags default (True), allow_redirects off",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "curl-etag",
          "name": "curl --etag-compare / --etag-save",
          "role": "client",
          "class": "cli-conditional",
          "version": "8.7.1",
          "library": "curl",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node cli.mjs curl-etag",
          "configuration": "curl -q -sS --http1.1 --etag-save etag.txt --etag-compare etag.txt -o body.bin -w '%{http_code} %{size_download}' <url>",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "curl-z",
          "name": "curl -z",
          "role": "client",
          "class": "cli-conditional",
          "version": "8.7.1",
          "library": "curl",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node cli.mjs curl-z",
          "configuration": "curl -q -sS --http1.1 -z body.bin -o body.bin -w '%{http_code} %{size_download}' <url>",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "got-cache",
          "name": "got + cacheable-request",
          "role": "client",
          "class": "library-cache",
          "version": "16.0.0",
          "library": "got",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node node-caches.mjs got-cache",
          "configuration": "got.extend({cache: new Map(), retry: {limit: 0}, followRedirect: false, throwHttpErrors: false}), cacheable-request default",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "gregjones-httpcache",
          "name": "gregjones/httpcache",
          "role": "client",
          "class": "library-cache",
          "version": "v0.0.0-20190611155906-901d90724c79",
          "library": "gregjones/httpcache",
          "runtime": "go1.27.0",
          "platform": "Darwin arm64",
          "invocation": "gotool gregjones-httpcache",
          "configuration": "httpcache.NewTransport(diskcache.New(<directory in the working directory>)) over an http.Transport built from scratch (ForceAttemptHTTP2 false, empty TLSNextProto), MarkCachedResponses default (true), redirects not followed",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "hishel",
          "name": "hishel",
          "role": "client",
          "class": "library-cache",
          "version": "1.4.0 (httpx 0.28.1)",
          "library": "hishel (httpx)",
          "runtime": "python 3.14.7",
          "platform": "Darwin arm64",
          "invocation": "python -I py-caches.py hishel",
          "configuration": "hishel.httpx.SyncCacheClient(storage=SyncSqliteStorage(database_path=a file in the working directory)), default policy, follow_redirects off; the driver waits 1100 ms before every fetch after the first",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "make-fetch-happen",
          "name": "make-fetch-happen",
          "role": "client",
          "class": "library-cache",
          "version": "16.0.1",
          "library": "make-fetch-happen",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node node-caches.mjs make-fetch-happen",
          "configuration": "make-fetch-happen defaults({cachePath: a directory in the working directory}), cache mode default, no other options",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "nginx-stale",
          "name": "nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate)",
          "role": "client",
          "class": "proxy-cache",
          "version": "1.31.6",
          "library": "nginx",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node proxy.mjs nginx-stale",
          "configuration": "nginx proxy_pass to the recorder; proxy_cache_path <dir>/cache keys_zone=etagwitness:1m; proxy_cache etagwitness; proxy_cache_revalidate on; proxy_cache_valid 200 1s; proxy_ignore_headers Cache-Control; the driver waits 2300 ms before every fetch after the first; add_header X-Cache-Status $upstream_cache_status always (observation only); nginx defaults otherwise (config: proxies/nginx.conf.tmpl)",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "requests-cache-cc",
          "name": "requests-cache (cache_control=True)",
          "role": "client",
          "class": "library-cache",
          "version": "1.3.3 (requests 2.34.2)",
          "library": "requests-cache",
          "runtime": "python 3.14.7",
          "platform": "Darwin arm64",
          "invocation": "python -I py-caches.py requests-cache-cc",
          "configuration": "requests_cache.CachedSession('<file in the working directory>', backend='sqlite', cache_control=True), other options default, allow_redirects off",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "squid",
          "name": "Squid",
          "role": "client",
          "class": "proxy-cache",
          "version": "7.7",
          "library": "Squid",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node proxy.mjs squid",
          "configuration": "squid -N, forward proxy: http_port on its own loopback listen port; visible_hostname etag-witness-squid; pid_filename, cache_log, access_log none, coredump_dir in <dir>; shutdown_lifetime 1 seconds; http_access allow for its built-in loopback client ACL (the curl user), deny all; no cache_dir (default in-memory cache); reply_header_add X-Cache-Status \"%Ss\" (observation only); curl uses it with -x ",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "trafficserver",
          "name": "Apache Traffic Server (required_headers 0)",
          "role": "client",
          "class": "proxy-cache",
          "version": "10.2.0-0",
          "library": "Apache Traffic Server",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node proxy.mjs trafficserver",
          "configuration": "traffic_server --run-root <dir>/ats/runroot.yaml with the package's shipped configuration files and four rendered over them: records.yaml (http.server_ports its own loopback listen port, http.cache.http 1, http.cache.required_headers 0, reverse_proxy.enabled 1, url_remap.remap_required 1, http.insert_response_via_str 2 observation only), remap.config (map the proxy to the recorder), storage.config",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "undici-cache",
          "name": "undici",
          "role": "client",
          "class": "library-cache",
          "version": "8.11.2",
          "library": "undici (interceptors.cache)",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node node-caches.mjs undici-cache",
          "configuration": "undici request(), Agent composed with interceptors.cache(), default options (MemoryCacheStore, type shared, methods GET)",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "varnish",
          "name": "Varnish",
          "role": "client",
          "class": "proxy-cache",
          "version": "9.1.0",
          "library": "Varnish Cache",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node proxy.mjs varnish",
          "configuration": "varnishd -F -n <dir>/varnish -a <its own loopback listen port> -f varnish.vcl, the recorder as the only backend (backend default only; the builtin VCL is Varnish's, appended as always); malloc storage and parameters default (config: proxies/varnish.vcl.tmpl)",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        },
        {
          "id": "wget-N",
          "name": "GNU Wget -N",
          "role": "client",
          "class": "cli-conditional",
          "version": "1.25.0",
          "library": "GNU Wget",
          "runtime": "node v26.10.0",
          "platform": "Darwin arm64",
          "invocation": "node cli.mjs wget-N",
          "configuration": "wget --no-config -N -t 1 --max-redirect=0 --server-response <url> (output file named by wget from the URL, in the working directory)",
          "requests_recorded_by": "scripts/witness-recorder.mjs (a local recording reverse proxy: plain HTTP/1.1 from the client, verbatim to https://badhttp.dev over TLS)"
        }
      ],
      "fields": [
        "id",
        "corpus_id",
        "family",
        "flavor",
        "url",
        "client",
        "observed",
        "badhttp_version_observed",
        "status",
        "reported_error",
        "outcome",
        "license",
        "class",
        "scenario",
        "fetches",
        "exchanges_after_result",
        "late_exchanges",
        "end",
        "wall_ms",
        "attempts",
        "pattern",
        "adopted_304_validator",
        "outcome_vs_reference",
        "reference",
        "control",
        "oracle",
        "configuration"
      ],
      "class_legend": {
        "library-cache": "A caching layer under an HTTP client library: it stores a response and decides, from what it stored, whether to revalidate and what to send. The harness never sends a conditional header itself, so every If-None-Match and If-Modified-Since in the row is one the library chose.",
        "cli-conditional": "A command-line tool that keeps one saved validator between runs (curl --etag-save/--etag-compare, curl -z, wget -N). Not a cache in the RFC 9111 sense: it has no store of responses, only a validator and a file, so the three fetches are three runs in one working directory. The row names the exact flags in configuration.",
        "proxy-cache": "A caching proxy configured with the recorder as its upstream and driven by curl as its user, with only the directives that enable its cache at all (named in configuration). The exchanges are what the proxy sent upstream; the caller is curl talking to the proxy.",
        "raw": "The control: curl sending exactly what the row says and nothing else (cold-raw: no conditional header; revalidate-raw: If-None-Match with the ETag cold-raw received and If-Modified-Since with the Last-Modified it received, verbatim, as RFC 9111 §4.3.1 says to validate; revalidate-adopted-raw on mismatch: the tag the 304 carried). Its body hash is the document every caller body is compared with."
      },
      "outcome_legend": {
        "document/stored-after-304": "The warm fetch put a request upstream, was answered 304, and the caller still received the flavor's document (the bytes of the cold control's body): the cache filled the body in from its store with status 200, or a command-line tool reported 304 and left its stored file.",
        "document/fresh-200": "The warm fetch put a request upstream, was answered 200, and the caller received the document from that response: the cache refetched, whether or not the request was conditional (wire.conditional says which).",
        "document/no-request": "The warm fetch put no request upstream and the caller received the document from the store, without revalidation.",
        "document/stored-despite-200": "The warm fetch put a request upstream and was answered 200 with a body, yet the tool reported 304 and kept its stored copy of the document (description.caller_status is 304).",
        "empty-304": "The caller received a 304 (the status, or an empty body with a 304 reported) rather than a body.",
        "other-body": "The caller received a 200 whose body is not the flavor's document; its bytes and sha256 are on the fetch.",
        "other-status": "The caller saw a status other than 200 and 304 on the warm fetch (for example a proxy's 503); its bytes and sha256 are on the fetch.",
        "error": "The library raised or the tool exited non-zero on the warm fetch; error.name is on the fetch.",
        "harness-timeout": "A harness cap fired (20 s for a fetch, 60 s for a cell); the row keeps the fetches that completed and marks the rest not-run.",
        "request-failed": "No observation: the rate limit never cleared, or the recorder saw an exchange without x-badhttp-version. Such a row has no exchanges and no fetch results.",
        "control": "A curl control row: one exchange, no cache. It is the source of the reference (cold-raw, revalidate-raw, revalidate-adopted-raw) and of the true document."
      },
      "description_legend": {
        "document": "The caller received the flavor's document and no error, with status 200, or with 304 reported by a tool that left its stored file. via says how: fresh-200, stored-after-304, stored-despite-200 or no-request.",
        "empty-304": "The caller received a 304 with no body: status 304 with 0 bytes, or 0 bytes with the last answer 304.",
        "other-body": "The caller received a 200 whose body is not the document.",
        "other-status": "The caller saw a status other than 200 and 304 (status, bytes and sha256 are on the description).",
        "error": "The library raised or the tool exited non-zero.",
        "not-run": "A cap fired before this fetch."
      },
      "via_legend": {
        "fresh-200": "The fetch's last request was answered 200.",
        "stored-after-304": "The fetch's last request was answered 304 and the caller still got the document: the cache filled it in from its store, or a command-line tool reported 304 and left its stored file (description.caller_status is 304 then).",
        "stored-despite-200": "The fetch's last request was answered 200 with a body, yet the tool reported 304 and kept its stored copy (description.caller_status is 304).",
        "no-request": "No request went upstream: the document was served from the store without revalidating."
      },
      "conditional_legend": {
        "none": "The first request carried neither If-None-Match nor If-Modified-Since: an unconditional request.",
        "etag": "The first request carried If-None-Match only.",
        "date": "The first request carried If-Modified-Since only.",
        "both": "The first request carried both If-None-Match and If-Modified-Since.",
        "if-match": "The first request carried an If-Match (recorded, not expected from a cache)."
      },
      "echo_legend": {
        "etag_echo": {
          "verbatim": "The If-None-Match value is the ETag the client last received in a 200, byte for byte.",
          "weak-stripped": "The client received W/\"x\" and sent \"x\".",
          "weak-added": "The client received \"x\" and sent W/\"x\".",
          "quotes-added": "The client received a bare x and sent \"x\".",
          "quotes-removed": "The client received \"x\" and sent a bare x.",
          "list": "The client sent a list that contains the received tag.",
          "other": "The client sent a tag that no 200 it received carried: the tag from a 304 (on mismatch), a tag taken from an unconditional 304, or a tag with no 200 having carried one (an empty tag, say). etag_sent and etag_received are on the wire; etag_received is null when no 200 carried a tag."
        },
        "date_echo": {
          "verbatim": "The If-Modified-Since value is the Last-Modified the client last received in a 200, byte for byte.",
          "reformatted": "A valid HTTP-date for the same instant, different bytes.",
          "own-clock": "A valid HTTP-date for a different instant, such as the response's Date or the current time.",
          "invalid-echoed": "A value that is not an HTTP-date, sent back as received (bad-date's ISO string).",
          "other": "Another value, including a date sent when no 200 carried a Last-Modified (date_received is null then)."
        }
      },
      "wire_legend": {
        "etag_held": "The ETag of the most recent 200 when the fetch began (null if none).",
        "date_held": "The Last-Modified of the most recent 200 when the fetch began (null if none).",
        "etag_dropped": "True when an ETag was held and the fetch's first request did not send it (present when requests is at least 1). A null etag_echo with etag_dropped false means nothing was held or nothing was sent.",
        "date_dropped": "True when a Last-Modified was held and the fetch's first request did not send it (present when requests is at least 1)."
      },
      "reference_legend": {
        "as-reference": "The warm fetch matches the reference: what RFC 9111 implies a textbook cache would show its caller on the warm fetch, given what the control's textbook revalidation got.",
        "differs": "The warm fetch does not match the reference. This is a description: RFC 9111 §5.2.2.4 forbids reusing a no-cache response without validation and does not forbid an unconditional refetch, so a cache that refetches and one that revalidates are both allowed to.",
        "undefined": "No reference exists: RFC 9110 §15.4.5 defines a 304 only in response to a conditional request, so the cold 304 of always-304 selects no stored response and RFC 9111 §4.3.4 says nothing about what a cache should show; the rows describe what each cache did."
      },
      "reading_this": "Read outcome as a description of what the caller received on the warm fetch and what went upstream, never as a verdict on the client. RFC 9111 §4.3 says what a cache sends to validate a stored response and what a 304 and a 200 mean to it, and §5.2.2.4 says a no-cache response must not be reused without a successful validation; it does not forbid an unconditional refetch, so a cache that revalidates and one that refetches are both doing something the specification allows. Each row is described relative to a reference derived from the control's own exchanges: what a textbook cache would have sent and shown its caller given what this server answered curl's textbook revalidation. \"differs\" is a description of a difference from that reference, not a defect; where the specification defines nothing (a 304 to a request that carried no conditional, an unparseable Last-Modified, a bare ETag) the reference says so. The harness never sends a conditional header for a client, so every validator in a row is one the client chose from what it stored. Not every class is a cache in the RFC 9111 sense: a library cache stores responses, a command-line conditional client keeps one saved validator, and a caching proxy is driven by curl as its user; each row names its class. The recorder sits between the client and this service and speaks plain HTTP/1.1 to the client, so TLS and HTTP/2 behaviour is out of scope; for a proxy the exchanges are what it sent upstream. No client here is scored or ranked.",
      "outcome_counts": {
        "control": 21,
        "document/stored-after-304": 78,
        "document/fresh-200": 45,
        "document/stored-despite-200": 3,
        "empty-304": 12,
        "other-status": 1,
        "error": 1
      },
      "class_counts": {
        "raw": 1,
        "library-cache": 7,
        "cli-conditional": 3,
        "proxy-cache": 4
      },
      "conditional_counts": {
        "both": 59,
        "etag": 20,
        "date": 36,
        "none": 25
      },
      "etag_echo_counts": {
        "verbatim": 128,
        "other": 21,
        "weak-stripped": 2
      },
      "date_echo_counts": {
        "verbatim": 143,
        "own-clock": 30,
        "invalid-echoed": 10
      },
      "reference": {
        "ok": {
          "flavor": "ok",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 307,
            "sha256": "74de73e44ca643ef487e843092db2109b1f3e37d16837ce8f2c231695391c009"
          },
          "cold": {
            "status": 200,
            "etag": "\"e-badhttp-1\"",
            "last_modified": "Sat, 01 Aug 2026 00:00:00 GMT",
            "bytes": 307
          },
          "revalidate": {
            "sent": {
              "if_none_match": "\"e-badhttp-1\"",
              "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
            },
            "status": 304,
            "etag": "\"e-badhttp-1\"",
            "last_modified": null,
            "bytes": 0
          },
          "expected_warm_conditional": "both",
          "outcome": "document/stored-after-304",
          "validator_update": "confirmed",
          "next_conditional": {
            "if_none_match": "\"e-badhttp-1\"",
            "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
          },
          "rule": "RFC 9111 §4.1, §4.3.1 and §4.3.3: a stored no-cache response (§5.2.2.4) is validated before reuse, with If-None-Match carrying the stored ETag and If-Modified-Since the stored Last-Modified value; a 304 indicates that the stored response can be updated and reused (§4.3.3), so the caller is handed the stored document. Which stored response the 304 updates is §4.3.4's question, answered by validator_update: confirmed when the 304 repeats the stored validator; none when it carries a different ETag or no validator, in which case §4.3.4 selects nothing to update and RFC 9111 does not say whether the stored response is still handed on — this reference follows §4.3.3's reading, and a cache that drops the entry and refetches instead is described as differing from it for that reason.",
          "note": null
        },
        "weak": {
          "flavor": "weak",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 333,
            "sha256": "960c16c451b93b0c6011f44f9ab60b41408e40c3e962423c89cb78fd29582deb"
          },
          "cold": {
            "status": 200,
            "etag": "W/\"e-badhttp-1\"",
            "last_modified": "Sat, 01 Aug 2026 00:00:00 GMT",
            "bytes": 333
          },
          "revalidate": {
            "sent": {
              "if_none_match": "W/\"e-badhttp-1\"",
              "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
            },
            "status": 304,
            "etag": "W/\"e-badhttp-1\"",
            "last_modified": null,
            "bytes": 0
          },
          "expected_warm_conditional": "both",
          "outcome": "document/stored-after-304",
          "validator_update": "confirmed",
          "next_conditional": {
            "if_none_match": "W/\"e-badhttp-1\"",
            "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
          },
          "rule": "RFC 9111 §4.1, §4.3.1 and §4.3.3: a stored no-cache response (§5.2.2.4) is validated before reuse, with If-None-Match carrying the stored ETag and If-Modified-Since the stored Last-Modified value; a 304 indicates that the stored response can be updated and reused (§4.3.3), so the caller is handed the stored document. Which stored response the 304 updates is §4.3.4's question, answered by validator_update: confirmed when the 304 repeats the stored validator; none when it carries a different ETag or no validator, in which case §4.3.4 selects nothing to update and RFC 9111 does not say whether the stored response is still handed on — this reference follows §4.3.3's reading, and a cache that drops the entry and refetches instead is described as differing from it for that reason.",
          "note": null
        },
        "changing": {
          "flavor": "changing",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 332,
            "sha256": "a4b0640490d0945371ed46abfdc246a8c8d835439bd3337556e216bfee0a3fb9"
          },
          "cold": {
            "status": 200,
            "etag": "\"e-1791834252567568\"",
            "last_modified": null,
            "bytes": 332
          },
          "revalidate": {
            "sent": {
              "if_none_match": "\"e-1791834252567568\"",
              "if_modified_since": null
            },
            "status": 200,
            "etag": "\"e-179183425385602b\"",
            "last_modified": null,
            "bytes": 332
          },
          "expected_warm_conditional": "etag",
          "outcome": "document/fresh-200",
          "validator_update": null,
          "next_conditional": {
            "if_none_match": "\"e-179183425385602b\"",
            "if_modified_since": null
          },
          "rule": "RFC 9111 §4.3.3: a 200 to a validation request replaces the stored response, and the new response is what the cache hands on.",
          "note": "the 200 carries a different ETag than the cold response, and a cache that stores it sends that tag next"
        },
        "ignore": {
          "flavor": "ignore",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 306,
            "sha256": "41bbf60812c9284ed440a4fc32054ce2d952dbad66baa5ae90e4efb5460d8ac3"
          },
          "cold": {
            "status": 200,
            "etag": "\"e-badhttp-1\"",
            "last_modified": "Sat, 01 Aug 2026 00:00:00 GMT",
            "bytes": 306
          },
          "revalidate": {
            "sent": {
              "if_none_match": "\"e-badhttp-1\"",
              "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
            },
            "status": 200,
            "etag": "\"e-badhttp-1\"",
            "last_modified": "Sat, 01 Aug 2026 00:00:00 GMT",
            "bytes": 306
          },
          "expected_warm_conditional": "both",
          "outcome": "document/fresh-200",
          "validator_update": null,
          "next_conditional": {
            "if_none_match": "\"e-badhttp-1\"",
            "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
          },
          "rule": "RFC 9111 §4.3.3: a 200 to a validation request replaces the stored response, and the new response is what the cache hands on.",
          "note": null
        },
        "always-304": {
          "flavor": "always-304",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": null,
          "cold": {
            "status": 304,
            "etag": "\"e-badhttp-1\"",
            "last_modified": null,
            "bytes": 0
          },
          "revalidate": {
            "sent": {
              "if_none_match": "\"e-badhttp-1\"",
              "if_modified_since": null
            },
            "status": 304,
            "etag": "\"e-badhttp-1\"",
            "last_modified": null,
            "bytes": 0
          },
          "expected_warm_conditional": "etag",
          "outcome": "undefined",
          "validator_update": null,
          "next_conditional": null,
          "rule": "RFC 9110 §15.4.5 defines 304 as the answer to a conditional request, and RFC 9111 §4.3.4 selects no stored response when none exists, so the specification does not say what a cache hands its caller; the rows describe what each cache did.",
          "note": "the cold request was answered 304 although it carried no conditional header, so no document exists for this flavor and no reference outcome is defined"
        },
        "mismatch": {
          "flavor": "mismatch",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 311,
            "sha256": "8c76d92aa26ad5dc44c33f68bdbf2f4473be3eaead00a2ba96fbe6058ea247fc"
          },
          "cold": {
            "status": 200,
            "etag": "\"e-badhttp-1\"",
            "last_modified": "Sat, 01 Aug 2026 00:00:00 GMT",
            "bytes": 311
          },
          "revalidate": {
            "sent": {
              "if_none_match": "\"e-badhttp-1\"",
              "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
            },
            "status": 304,
            "etag": "\"e-badhttp-2\"",
            "last_modified": null,
            "bytes": 0
          },
          "expected_warm_conditional": "both",
          "outcome": "document/stored-after-304",
          "validator_update": "none",
          "next_conditional": {
            "if_none_match": "\"e-badhttp-1\"",
            "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
          },
          "rule": "RFC 9111 §4.1, §4.3.1 and §4.3.3: a stored no-cache response (§5.2.2.4) is validated before reuse, with If-None-Match carrying the stored ETag and If-Modified-Since the stored Last-Modified value; a 304 indicates that the stored response can be updated and reused (§4.3.3), so the caller is handed the stored document. Which stored response the 304 updates is §4.3.4's question, answered by validator_update: confirmed when the 304 repeats the stored validator; none when it carries a different ETag or no validator, in which case §4.3.4 selects nothing to update and RFC 9111 does not say whether the stored response is still handed on — this reference follows §4.3.3's reading, and a cache that drops the entry and refetches instead is described as differing from it for that reason.",
          "note": "the 304 carries a different ETag than the one sent, so its validators select no stored response and a cache that keeps its stored validator sends the same tag again",
          "adopted": {
            "sent": "\"e-badhttp-2\"",
            "status": 200,
            "etag": "\"e-badhttp-1\""
          }
        },
        "no-validator-304": {
          "flavor": "no-validator-304",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 335,
            "sha256": "e5ff076d6dc51ff78c8a268dfc9aa048fb1d7ad0a3ceb660d17c2fcd270436b3"
          },
          "cold": {
            "status": 200,
            "etag": "\"e-badhttp-1\"",
            "last_modified": "Sat, 01 Aug 2026 00:00:00 GMT",
            "bytes": 335
          },
          "revalidate": {
            "sent": {
              "if_none_match": "\"e-badhttp-1\"",
              "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
            },
            "status": 304,
            "etag": null,
            "last_modified": null,
            "bytes": 0
          },
          "expected_warm_conditional": "both",
          "outcome": "document/stored-after-304",
          "validator_update": "none",
          "next_conditional": {
            "if_none_match": "\"e-badhttp-1\"",
            "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
          },
          "rule": "RFC 9111 §4.1, §4.3.1 and §4.3.3: a stored no-cache response (§5.2.2.4) is validated before reuse, with If-None-Match carrying the stored ETag and If-Modified-Since the stored Last-Modified value; a 304 indicates that the stored response can be updated and reused (§4.3.3), so the caller is handed the stored document. Which stored response the 304 updates is §4.3.4's question, answered by validator_update: confirmed when the 304 repeats the stored validator; none when it carries a different ETag or no validator, in which case §4.3.4 selects nothing to update and RFC 9111 does not say whether the stored response is still handed on — this reference follows §4.3.3's reading, and a cache that drops the entry and refetches instead is described as differing from it for that reason.",
          "note": "the 304 carries no ETag, so its validators select no stored response and a cache keeps the stored validator"
        },
        "unquoted": {
          "flavor": "unquoted",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 322,
            "sha256": "b4cc8df08aa33f68b9689e824c693e4a453612ffcf3a8ce1a2916cc117973daf"
          },
          "cold": {
            "status": 200,
            "etag": "e-badhttp-1",
            "last_modified": "Sat, 01 Aug 2026 00:00:00 GMT",
            "bytes": 322
          },
          "revalidate": {
            "sent": {
              "if_none_match": "e-badhttp-1",
              "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
            },
            "status": 304,
            "etag": "e-badhttp-1",
            "last_modified": null,
            "bytes": 0
          },
          "expected_warm_conditional": "both",
          "outcome": "document/stored-after-304",
          "validator_update": "confirmed",
          "next_conditional": {
            "if_none_match": "e-badhttp-1",
            "if_modified_since": "Sat, 01 Aug 2026 00:00:00 GMT"
          },
          "rule": "RFC 9111 §4.1, §4.3.1 and §4.3.3: a stored no-cache response (§5.2.2.4) is validated before reuse, with If-None-Match carrying the stored ETag and If-Modified-Since the stored Last-Modified value; a 304 indicates that the stored response can be updated and reused (§4.3.3), so the caller is handed the stored document. Which stored response the 304 updates is §4.3.4's question, answered by validator_update: confirmed when the 304 repeats the stored validator; none when it carries a different ETag or no validator, in which case §4.3.4 selects nothing to update and RFC 9111 does not say whether the stored response is still handed on — this reference follows §4.3.3's reading, and a cache that drops the entry and refetches instead is described as differing from it for that reason.",
          "note": "the ETag is a bare token without quotes; the reference echoes it as received"
        },
        "bad-date": {
          "flavor": "bad-date",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 331,
            "sha256": "e471d0a68b161892890c1c722eef8d21d34736eb4e358c6a373b5ff188e2ea98"
          },
          "cold": {
            "status": 200,
            "etag": null,
            "last_modified": "2026-08-01T00:00:00Z",
            "bytes": 331
          },
          "revalidate": {
            "sent": {
              "if_none_match": null,
              "if_modified_since": "2026-08-01T00:00:00Z"
            },
            "status": 304,
            "etag": null,
            "last_modified": "2026-08-01T00:00:00Z",
            "bytes": 0
          },
          "expected_warm_conditional": "date",
          "outcome": "document/stored-after-304",
          "validator_update": "confirmed",
          "next_conditional": {
            "if_none_match": null,
            "if_modified_since": "2026-08-01T00:00:00Z"
          },
          "rule": "RFC 9111 §4.1, §4.3.1 and §4.3.3: a stored no-cache response (§5.2.2.4) is validated before reuse, with If-None-Match carrying the stored ETag and If-Modified-Since the stored Last-Modified value; a 304 indicates that the stored response can be updated and reused (§4.3.3), so the caller is handed the stored document. Which stored response the 304 updates is §4.3.4's question, answered by validator_update: confirmed when the 304 repeats the stored validator; none when it carries a different ETag or no validator, in which case §4.3.4 selects nothing to update and RFC 9111 does not say whether the stored response is still handed on — this reference follows §4.3.3's reading, and a cache that drops the entry and refetches instead is described as differing from it for that reason.",
          "note": "the Last-Modified value is not an HTTP-date; RFC 9111 §4.3.1 says to send the stored value, so this reference rests on echoing that value as received, and a cache that drops the unparseable date and refetches is described as differing from it for that reason"
        },
        "future": {
          "flavor": "future",
          "source": "derived from the cold-raw and revalidate-raw control exchanges of this flavor",
          "document": {
            "bytes": 333,
            "sha256": "2df14f3a6d244dd5ff2b092a6f3d7d546f29a8ce7b87a31d4c35e12888d27174"
          },
          "cold": {
            "status": 200,
            "etag": null,
            "last_modified": "Tue, 12 Oct 2027 00:00:00 GMT",
            "bytes": 333
          },
          "revalidate": {
            "sent": {
              "if_none_match": null,
              "if_modified_since": "Tue, 12 Oct 2027 00:00:00 GMT"
            },
            "status": 304,
            "etag": null,
            "last_modified": "Tue, 12 Oct 2027 00:00:00 GMT",
            "bytes": 0
          },
          "expected_warm_conditional": "date",
          "outcome": "document/stored-after-304",
          "validator_update": "confirmed",
          "next_conditional": {
            "if_none_match": null,
            "if_modified_since": "Tue, 12 Oct 2027 00:00:00 GMT"
          },
          "rule": "RFC 9111 §4.1, §4.3.1 and §4.3.3: a stored no-cache response (§5.2.2.4) is validated before reuse, with If-None-Match carrying the stored ETag and If-Modified-Since the stored Last-Modified value; a 304 indicates that the stored response can be updated and reused (§4.3.3), so the caller is handed the stored document. Which stored response the 304 updates is §4.3.4's question, answered by validator_update: confirmed when the 304 repeats the stored validator; none when it carries a different ETag or no validator, in which case §4.3.4 selects nothing to update and RFC 9111 does not say whether the stored response is still handed on — this reference follows §4.3.3's reading, and a cache that drops the entry and refetches instead is described as differing from it for that reason.",
          "note": null
        }
      },
      "disagreement_by_flavor": {
        "ok": 5,
        "weak": 6,
        "changing": 4,
        "ignore": 5,
        "always-304": 4,
        "mismatch": 6,
        "no-validator-304": 5,
        "unquoted": 5,
        "bad-date": 5,
        "future": 4
      },
      "answers_by_flavor": {
        "ok": [
          {
            "answer": "document/stored-after-304 | both | verbatim | verbatim",
            "clients": [
              "cachecontrol",
              "got-cache",
              "gregjones-httpcache",
              "hishel",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "squid",
              "trafficserver",
              "undici-cache"
            ]
          },
          {
            "answer": "document/stored-after-304 | etag | verbatim | -",
            "clients": [
              "curl-etag"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | own-clock",
            "clients": [
              "curl-z"
            ]
          },
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "varnish"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | verbatim",
            "clients": [
              "wget-N"
            ]
          }
        ],
        "weak": [
          {
            "answer": "document/stored-after-304 | both | verbatim | verbatim",
            "clients": [
              "cachecontrol",
              "got-cache",
              "gregjones-httpcache",
              "hishel",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "undici-cache"
            ]
          },
          {
            "answer": "document/stored-after-304 | etag | verbatim | -",
            "clients": [
              "curl-etag"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | own-clock",
            "clients": [
              "curl-z"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | verbatim",
            "clients": [
              "squid",
              "wget-N"
            ]
          },
          {
            "answer": "document/stored-after-304 | both | weak-stripped | verbatim",
            "clients": [
              "trafficserver"
            ]
          },
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "varnish"
            ]
          }
        ],
        "changing": [
          {
            "answer": "document/fresh-200 | etag | verbatim | -",
            "clients": [
              "cachecontrol",
              "curl-etag",
              "got-cache",
              "gregjones-httpcache",
              "hishel",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "trafficserver"
            ]
          },
          {
            "answer": "document/fresh-200 | date | - | own-clock",
            "clients": [
              "curl-z",
              "wget-N"
            ]
          },
          {
            "answer": "document/fresh-200 | both | verbatim | own-clock",
            "clients": [
              "squid",
              "undici-cache"
            ]
          },
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "varnish"
            ]
          }
        ],
        "ignore": [
          {
            "answer": "document/fresh-200 | both | verbatim | verbatim",
            "clients": [
              "cachecontrol",
              "got-cache",
              "gregjones-httpcache",
              "hishel",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "squid",
              "trafficserver",
              "undici-cache"
            ]
          },
          {
            "answer": "document/fresh-200 | etag | verbatim | -",
            "clients": [
              "curl-etag"
            ]
          },
          {
            "answer": "document/stored-despite-200 | date | - | own-clock",
            "clients": [
              "curl-z"
            ]
          },
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "varnish"
            ]
          },
          {
            "answer": "document/stored-despite-200 | date | - | verbatim",
            "clients": [
              "wget-N"
            ]
          }
        ],
        "always-304": [
          {
            "answer": "empty-304 | none | - | -",
            "clients": [
              "cachecontrol",
              "curl-z",
              "hishel",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "squid",
              "trafficserver",
              "undici-cache"
            ]
          },
          {
            "answer": "empty-304 | etag | other | -",
            "clients": [
              "curl-etag",
              "got-cache",
              "gregjones-httpcache"
            ]
          },
          {
            "answer": "other-status | none | - | -",
            "clients": [
              "varnish"
            ]
          },
          {
            "answer": "error | none | - | -",
            "clients": [
              "wget-N"
            ]
          }
        ],
        "mismatch": [
          {
            "answer": "document/stored-after-304 | both | verbatim | verbatim",
            "clients": [
              "cachecontrol",
              "got-cache",
              "gregjones-httpcache",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "squid",
              "trafficserver",
              "undici-cache"
            ]
          },
          {
            "answer": "document/stored-after-304 | etag | verbatim | -",
            "clients": [
              "curl-etag"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | own-clock",
            "clients": [
              "curl-z"
            ]
          },
          {
            "answer": "document/fresh-200 | both | verbatim | verbatim",
            "clients": [
              "hishel"
            ]
          },
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "varnish"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | verbatim",
            "clients": [
              "wget-N"
            ]
          }
        ],
        "no-validator-304": [
          {
            "answer": "document/stored-after-304 | both | verbatim | verbatim",
            "clients": [
              "cachecontrol",
              "got-cache",
              "gregjones-httpcache",
              "hishel",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "squid",
              "trafficserver",
              "undici-cache"
            ]
          },
          {
            "answer": "document/stored-after-304 | etag | verbatim | -",
            "clients": [
              "curl-etag"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | own-clock",
            "clients": [
              "curl-z"
            ]
          },
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "varnish"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | verbatim",
            "clients": [
              "wget-N"
            ]
          }
        ],
        "unquoted": [
          {
            "answer": "document/stored-after-304 | both | verbatim | verbatim",
            "clients": [
              "cachecontrol",
              "got-cache",
              "gregjones-httpcache",
              "hishel",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "trafficserver"
            ]
          },
          {
            "answer": "document/stored-after-304 | etag | verbatim | -",
            "clients": [
              "curl-etag"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | own-clock",
            "clients": [
              "curl-z"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | verbatim",
            "clients": [
              "squid",
              "undici-cache",
              "wget-N"
            ]
          },
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "varnish"
            ]
          }
        ],
        "bad-date": [
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "cachecontrol",
              "nginx-stale",
              "trafficserver",
              "varnish"
            ]
          },
          {
            "answer": "document/fresh-200 | etag | other | -",
            "clients": [
              "curl-etag"
            ]
          },
          {
            "answer": "document/stored-despite-200 | date | - | own-clock",
            "clients": [
              "curl-z"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | invalid-echoed",
            "clients": [
              "got-cache",
              "gregjones-httpcache",
              "hishel",
              "make-fetch-happen",
              "requests-cache-cc"
            ]
          },
          {
            "answer": "document/fresh-200 | date | - | own-clock",
            "clients": [
              "squid",
              "undici-cache",
              "wget-N"
            ]
          }
        ],
        "future": [
          {
            "answer": "document/fresh-200 | none | - | -",
            "clients": [
              "cachecontrol",
              "undici-cache",
              "varnish"
            ]
          },
          {
            "answer": "document/fresh-200 | etag | other | -",
            "clients": [
              "curl-etag"
            ]
          },
          {
            "answer": "document/fresh-200 | date | - | own-clock",
            "clients": [
              "curl-z"
            ]
          },
          {
            "answer": "document/stored-after-304 | date | - | verbatim",
            "clients": [
              "got-cache",
              "gregjones-httpcache",
              "hishel",
              "make-fetch-happen",
              "nginx-stale",
              "requests-cache-cc",
              "squid",
              "trafficserver",
              "wget-N"
            ]
          }
        ]
      },
      "disagreement_note": "disagreement_by_flavor is, per flavor, the number of distinct answers — (outcome, conditional, etag_echo, date_echo) of the warm fetch — among client rows; answers_by_flavor lists them with the clients that gave each. conditional_counts is the first request of the warm fetch over all client rows (no-request when none went upstream); etag_echo_counts and date_echo_counts count the warm and warm-again fetches. Control rows are excluded.",
      "findings": [
        "14 HTTP cache or conditional-request clients (CacheControl, curl --etag-compare / --etag-save, curl -z, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), undici, Varnish, GNU Wget -N) were each pointed at /etag/{flavor} and asked for the same URL three times in a row through a fresh cache — 10 flavors, 140 rows in all. The harness never sends a conditional header for a client, so every If-None-Match and If-Modified-Since below is one the client chose from what it stored. By what the caller received on the second fetch: 78 document/stored-after-304, 45 document/fresh-200, 12 empty-304, 3 document/stored-despite-200, 1 other-status, 1 error. Against the reference (what RFC 9111 implies a textbook cache would have sent and shown, given what the control's textbook revalidation got), 104 rows match, 22 differ and 14 have no reference defined. RFC 9111 §5.2.2.4 forbids reusing a no-cache response without validation and does not forbid an unconditional refetch, so a row that differs is a description of a difference, not a finding about the client.",
        "ok (the control got, cold, 200 with ETag \"e-badhttp-1\", Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 307 body bytes; on a textbook revalidation, 304 with ETag \"e-badhttp-1\", no Last-Modified and 0 body bytes; reference: the document from the store after a 304, the 304 confirms the stored validators): on the warm fetch, CacheControl, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), undici sent If-None-Match (verbatim) and If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; curl --etag-compare / --etag-save sent If-None-Match (verbatim) but not the Last-Modified it had received, got 304 and left its stored file as the document and reported 304 to the caller; curl -z sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller; Varnish sent no conditional header, having received the ETag and the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; GNU Wget -N sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller. 13 of 14 rows match the reference and 1 differ.",
        "weak (the control got, cold, 200 with ETag W/\"e-badhttp-1\", Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 333 body bytes; on a textbook revalidation, 304 with ETag W/\"e-badhttp-1\", no Last-Modified and 0 body bytes; reference: the document from the store after a 304, the 304 confirms the stored validators): on the warm fetch, CacheControl, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici sent If-None-Match (verbatim) and If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; curl --etag-compare / --etag-save sent If-None-Match (verbatim) but not the Last-Modified it had received, got 304 and left its stored file as the document and reported 304 to the caller; curl -z sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller; Squid sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and handed the caller the document from its store; Apache Traffic Server (required_headers 0) sent If-None-Match (with the W/ stripped) and If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; Varnish sent no conditional header, having received the ETag and the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; GNU Wget -N sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller. 13 of 14 rows match the reference and 1 differ.",
        "changing (the control got, cold, 200 with ETag \"e-1791834252567568\", no Last-Modified and 332 body bytes; on a textbook revalidation, 200 with ETag \"e-179183425385602b\", no Last-Modified and 332 body bytes; reference: the document from the fresh 200 (the 200 carries a different ETag than the cold response, and a cache that stores it sends that tag next)): on the warm fetch, CacheControl, curl --etag-compare / --etag-save, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Apache Traffic Server (required_headers 0) sent If-None-Match (verbatim), got 200 and handed the caller the document from the response; curl -z, GNU Wget -N sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 200 and handed the caller the document from the response; Squid, undici sent If-None-Match (verbatim) and If-Modified-Since (as a date from its own clock), got 200 and handed the caller the document from the response; Varnish sent no conditional header, having received the ETag in an earlier 200, got 200 and handed the caller the document from the response. 14 of 14 rows match the reference and 0 differ.",
        "ignore (the control got, cold, 200 with ETag \"e-badhttp-1\", Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 306 body bytes; on a textbook revalidation, 200 with ETag \"e-badhttp-1\", Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 306 body bytes; reference: the document from the fresh 200): on the warm fetch, CacheControl, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), undici sent If-None-Match (verbatim) and If-Modified-Since (verbatim), got 200 and handed the caller the document from the response; curl --etag-compare / --etag-save sent If-None-Match (verbatim) but not the Last-Modified it had received, got 200 and handed the caller the document from the response; curl -z sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 200 and kept its stored copy and reported 304 to the caller, although the last answer was a 200 with a body; Varnish sent no conditional header, having received the ETag and the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; GNU Wget -N sent If-Modified-Since (verbatim) but not the ETag it had received, got 200 and kept its stored copy and reported 304 to the caller, although the last answer was a 200 with a body. 12 of 14 rows match the reference and 2 differ.",
        "always-304 (the control got, cold, 304 with ETag \"e-badhttp-1\", no Last-Modified and 0 body bytes; on a textbook revalidation, 304 with ETag \"e-badhttp-1\", no Last-Modified and 0 body bytes; reference: nothing defined (the cold request was answered 304 although it carried no conditional header, so no document exists for this flavor and no reference outcome is defined)): on the warm fetch, CacheControl, curl -z, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), undici sent no conditional header, got 304 and handed the caller a 304 with no body; curl --etag-compare / --etag-save, got + cacheable-request, gregjones/httpcache sent If-None-Match (a tag that no 200 it received carried), got 304 and handed the caller a 304 with no body; Varnish sent no conditional header, got 304 and handed the caller a response with status 503; GNU Wget -N sent no conditional header, got 304 and ended in an error (ServerError). 14 of 14 rows have no reference to match.",
        "mismatch (the control got, cold, 200 with ETag \"e-badhttp-1\", Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 311 body bytes; on a textbook revalidation, 304 with ETag \"e-badhttp-2\", no Last-Modified and 0 body bytes; reference: the document from the store after a 304, the 304's validators select no stored response, so the stored validator is kept (the 304 carries a different ETag than the one sent, so its validators select no stored response and a cache that keeps its stored validator sends the same tag again)): on the warm fetch, CacheControl, got + cacheable-request, gregjones/httpcache, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), undici sent If-None-Match (verbatim) and If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; curl --etag-compare / --etag-save sent If-None-Match (verbatim) but not the Last-Modified it had received, got 304 and left its stored file as the document and reported 304 to the caller; curl -z sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller; hishel sent If-None-Match (verbatim) and If-Modified-Since (verbatim) on the first of 2 requests, the last of which got 200 and handed the caller the document from the response; Varnish sent no conditional header, having received the ETag and the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; GNU Wget -N sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller. 12 of 14 rows match the reference and 2 differ.",
        "no-validator-304 (the control got, cold, 200 with ETag \"e-badhttp-1\", Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 335 body bytes; on a textbook revalidation, 304 with no ETag, no Last-Modified and 0 body bytes; reference: the document from the store after a 304, the 304's validators select no stored response, so the stored validator is kept (the 304 carries no ETag, so its validators select no stored response and a cache keeps the stored validator)): on the warm fetch, CacheControl, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), undici sent If-None-Match (verbatim) and If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; curl --etag-compare / --etag-save sent If-None-Match (verbatim) but not the Last-Modified it had received, got 304 and left its stored file as the document and reported 304 to the caller; curl -z sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller; Varnish sent no conditional header, having received the ETag and the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; GNU Wget -N sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller. 13 of 14 rows match the reference and 1 differ.",
        "unquoted (the control got, cold, 200 with ETag e-badhttp-1, Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 322 body bytes; on a textbook revalidation, 304 with ETag e-badhttp-1, no Last-Modified and 0 body bytes; reference: the document from the store after a 304, the 304 confirms the stored validators (the ETag is a bare token without quotes; the reference echoes it as received)): on the warm fetch, CacheControl, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Apache Traffic Server (required_headers 0) sent If-None-Match (verbatim) and If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; Squid, undici sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and handed the caller the document from its store; curl --etag-compare / --etag-save sent If-None-Match (verbatim) but not the Last-Modified it had received, got 304 and left its stored file as the document and reported 304 to the caller; curl -z sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller; Varnish sent no conditional header, having received the ETag and the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; GNU Wget -N sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller. 13 of 14 rows match the reference and 1 differ.",
        "bad-date (the control got, cold, 200 with no ETag, Last-Modified 2026-08-01T00:00:00Z and 331 body bytes; on a textbook revalidation, 304 with no ETag, Last-Modified 2026-08-01T00:00:00Z and 0 body bytes; reference: the document from the store after a 304, the 304 confirms the stored validators (the Last-Modified value is not an HTTP-date; RFC 9111 §4.3.1 says to send the stored value, so this reference rests on echoing that value as received, and a cache that drops the unparseable date and refetches is described as differing from it for that reason)): on the warm fetch, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, requests-cache (cache_control=True) sent If-Modified-Since (the invalid value echoed back), got 304 and handed the caller the document from its store; CacheControl, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), Apache Traffic Server (required_headers 0), Varnish sent no conditional header, having received the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; Squid, undici, GNU Wget -N sent If-Modified-Since (as a date from its own clock), got 200 and handed the caller the document from the response; curl --etag-compare / --etag-save sent If-None-Match (a tag that no 200 it received carried) but not the Last-Modified it had received, got 200 and handed the caller the document from the response; curl -z sent If-Modified-Since (as a date from its own clock), got 200 and kept its stored copy and reported 304 to the caller, although the last answer was a 200 with a body. 5 of 14 rows match the reference and 9 differ.",
        "future (the control got, cold, 200 with no ETag, Last-Modified Tue, 12 Oct 2027 00:00:00 GMT and 333 body bytes; on a textbook revalidation, 304 with no ETag, Last-Modified Tue, 12 Oct 2027 00:00:00 GMT and 0 body bytes; reference: the document from the store after a 304, the 304 confirms the stored validators): on the warm fetch, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0) sent If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; CacheControl, undici, Varnish sent no conditional header, having received the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; curl --etag-compare / --etag-save sent If-None-Match (a tag that no 200 it received carried) but not the Last-Modified it had received, got 200 and handed the caller the document from the response; curl -z sent If-Modified-Since (as a date from its own clock), got 200 and handed the caller the document from the response; GNU Wget -N sent If-Modified-Since (verbatim), got 304 and left its stored file as the document and reported 304 to the caller. 9 of 14 rows match the reference and 5 differ.",
        "Validators: of 422 requests the clients sent over the three fetches, 161 carried If-None-Match (Apache Traffic Server (required_headers 0), CacheControl, Squid, curl --etag-compare / --etag-save, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici) and 183 carried If-Modified-Since (Apache Traffic Server (required_headers 0), CacheControl, GNU Wget -N, Squid, curl -z, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici). On the first request of the warm fetch, of 140 rows: 20 carried If-None-Match only (Apache Traffic Server (required_headers 0), CacheControl, curl --etag-compare / --etag-save, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True)); 36 carried If-Modified-Since only (Apache Traffic Server (required_headers 0), GNU Wget -N, Squid, curl -z, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici); 59 carried both (Apache Traffic Server (required_headers 0), CacheControl, Squid, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici); 25 carried neither header (Apache Traffic Server (required_headers 0), CacheControl, GNU Wget -N, Squid, Varnish, curl -z, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici); 0 carried no request at all. A validator held from the first 200 and not sent on the warm fetch: the ETag in 24 rows (GNU Wget -N, Squid, Varnish, curl -z, undici), the Last-Modified in 21 rows (Apache Traffic Server (required_headers 0), CacheControl, Varnish, curl --etag-compare / --etag-save, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), undici).",
        "Echo: each If-None-Match or If-Modified-Since value is compared with the validator that client most recently received in a 200. Of the If-None-Match values sent on the warm and warm-again fetches, verbatim: 128 (Apache Traffic Server (required_headers 0), CacheControl, Squid, curl --etag-compare / --etag-save, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici); a tag that no 200 it received carried: 21 (Apache Traffic Server (required_headers 0), CacheControl, Squid, curl --etag-compare / --etag-save, got + cacheable-request, gregjones/httpcache, make-fetch-happen, requests-cache (cache_control=True)); with the W/ stripped: 2 (Apache Traffic Server (required_headers 0)). Of the If-Modified-Since values, verbatim: 143 (Apache Traffic Server (required_headers 0), CacheControl, GNU Wget -N, Squid, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici); as a date from its own clock: 30 (GNU Wget -N, Squid, curl -z, undici); the invalid value echoed back: 10 (got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, requests-cache (cache_control=True)).",
        "On weak (the control got, cold, 200 with ETag W/\"e-badhttp-1\", Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 333 body bytes), the If-None-Match of the warm fetch was: verbatim (CacheControl, curl --etag-compare / --etag-save, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), undici); not sent (curl -z, Squid, Varnish, GNU Wget -N); with the W/ stripped (Apache Traffic Server (required_headers 0)).",
        "On unquoted (the control got, cold, 200 with ETag e-badhttp-1, Last-Modified Sat, 01 Aug 2026 00:00:00 GMT and 322 body bytes), the If-None-Match of the warm fetch was: verbatim (CacheControl, curl --etag-compare / --etag-save, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Apache Traffic Server (required_headers 0)); not sent (curl -z, Squid, undici, Varnish, GNU Wget -N).",
        "On bad-date (the control got, cold, 200 with no ETag, Last-Modified 2026-08-01T00:00:00Z and 331 body bytes), the If-Modified-Since of the warm fetch was: not sent (CacheControl, curl --etag-compare / --etag-save, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), Apache Traffic Server (required_headers 0), Varnish); the invalid value echoed back (got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, requests-cache (cache_control=True)); as a date from its own clock (curl -z, Squid, undici, GNU Wget -N).",
        "On future (the control got, cold, 200 with no ETag, Last-Modified Tue, 12 Oct 2027 00:00:00 GMT and 333 body bytes), the If-Modified-Since of the warm fetch was: verbatim (got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), GNU Wget -N); not sent (CacheControl, curl --etag-compare / --etag-save, undici, Varnish); as a date from its own clock (curl -z).",
        "On mismatch (the control's revalidation sent If-None-Match \"e-badhttp-1\" and got 304 with ETag \"e-badhttp-2\", no Last-Modified and 0 body bytes, an ETag other than the one it sent), 6 of 14 clients sent, on a later fetch, a tag they had only ever received in that 304 (CacheControl, gregjones/httpcache, make-fetch-happen, requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0)). On their third fetch, CacheControl, gregjones/httpcache, make-fetch-happen, requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0) sent If-None-Match (a tag that no 200 it received carried) and If-Modified-Since (verbatim), got 200 and handed the caller the document from the response. The other 8 (curl --etag-compare / --etag-save, curl -z, got + cacheable-request, hishel, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), undici, Varnish, GNU Wget -N) did not. On their third fetch, got + cacheable-request, undici, Varnish sent no conditional header, having received the ETag and the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; curl --etag-compare / --etag-save sent If-None-Match (a tag that no 200 it received carried) but not the Last-Modified it had received, got 200 and handed the caller the document from the response; curl -z sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller; hishel sent If-None-Match (verbatim) and If-Modified-Since (verbatim) on the first of 2 requests, the last of which got 200 and handed the caller the document from the response; nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate) sent If-None-Match (verbatim) and If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; GNU Wget -N sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller.",
        "On always-304 the control's very first request, with no conditional header, got 304 with ETag \"e-badhttp-1\", no Last-Modified and 0 body bytes. On the cold fetch, CacheControl, curl -z, got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), undici sent no conditional header, got 304 and handed the caller a 304 with no body; curl --etag-compare / --etag-save sent If-None-Match (a tag that no 200 it received carried), got 304 and handed the caller a 304 with no body; Varnish sent no conditional header, got 304 and handed the caller a response with status 503; GNU Wget -N sent no conditional header, got 304 and ended in an error (ServerError). Across all three fetches, by pattern: CacheControl, curl -z, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0), undici →304 empty-304 | →304 empty-304 | →304 empty-304; got + cacheable-request, gregjones/httpcache →304 empty-304 | INM→304 empty-304 | INM→304 empty-304; curl --etag-compare / --etag-save INM→304 empty-304 | INM→304 empty-304 | INM→304 empty-304; Varnish →304 503 | →304 503 | →304 503; GNU Wget -N →304 error | →304 error | →304 error.",
        "On no-validator-304 the control's revalidation got 304 with no ETag, no Last-Modified and 0 body bytes. On the third fetch, after that bare 304, CacheControl, gregjones/httpcache, hishel, make-fetch-happen, nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate), requests-cache (cache_control=True), Squid sent If-None-Match (verbatim) and If-Modified-Since (verbatim), got 304 and handed the caller the document from its store; got + cacheable-request, undici, Varnish sent no conditional header, having received the ETag and the Last-Modified in an earlier 200, got 200 and handed the caller the document from the response; curl --etag-compare / --etag-save sent If-None-Match (a tag that no 200 it received carried) but not the Last-Modified it had received, got 200 and handed the caller the document from the response; curl -z sent If-Modified-Since (as a date from its own clock) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller; Apache Traffic Server (required_headers 0) sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and handed the caller the document from its store; GNU Wget -N sent If-Modified-Since (verbatim) but not the ETag it had received, got 304 and left its stored file as the document and reported 304 to the caller.",
        "No warm fetch was answered without a request going upstream: every client went to the server on the second and third fetch of every flavor.",
        "curl --etag-compare / --etag-save sent If-None-Match: \"\" on every cold fetch and again after every 304: when --etag-compare names a file that is missing or empty, curl 8.7.1 sends the literal empty tag (src/tool_operate.c, single_transfer), and --etag-save opens its file for writing before the transfer and writes an ETag line only for a 2xx response, so a 304 leaves the file empty. On ok, weak, mismatch, no-validator-304, unquoted the three fetches therefore went 200, 304, 200: the tag saved, matched once, lost with the 304, and saved again.",
        "curl -z never echoes a Last-Modified: libcurl rebuilds If-Modified-Since from the output file's modification time (lib/http.c, Curl_add_timecondition), which without -R is the time curl last wrote the file, so every date it sent here is its own clock. When a 200 then arrives whose Last-Modified is not newer than that time, or does not parse, libcurl discards the body and reports 304 itself (\"Simulate an HTTP 304 response\", Curl_http_readwrite_headers): the stored-despite-200 rows on ignore and bad-date.",
        "GNU Wget -N sends If-Modified-Since only when a local file exists, formatted from that file's modification time, which wget sets from a Last-Modified it can parse (src/http.c, gethttp): on ok the date went out as the server sent it, on bad-date (an ISO 8601 value its four strptime formats reject) as the download time; wget has no If-None-Match code at all. A 200 to its conditional request whose Last-Modified is not newer and whose Content-Length equals the file is dropped as \"Server ignored If-Modified-Since\" (the ignore row), and a cold 304 is \"ERROR 304: Not Modified.\" with exit 8.",
        "hishel made two requests inside each warm fetch of mismatch: its 304 handling (hishel/_core/_spec.py, freshening_stored_responses) matches a strong 304 ETag against the stored entry's tag, a 304 whose tag matches nothing deletes the entry, and the same call then sends a plain GET — so the caller got the fresh 200 and hishel never sent \"e-badhttp-2\".",
        "undici deletes the stored entry after a successful revalidation whose 304 carries a Cache-Control header (lib/interceptor/cache.js, revalidationResponseUpdatesCacheControl) — and this server's 304s all do — so its third fetch was a cold unconditional request on every flavor that answered the second with a 304, and what that 304 carried (a different ETag, or none) never reached its store.",
        "undici stored nothing on future (lib/handler/cache-handler.js, determineStaleAt returns early for a Last-Modified at or after now), sent If-Modified-Since alone on unquoted (only a quoted or W/-prefixed tag is a usable ETag to it) and, on bad-date, an If-Modified-Since built from the time it cached the entry rather than the ISO string (makeRevalidationHeaders falls back to cachedAt when the stored Last-Modified does not parse).",
        "got + cacheable-request stored the cold 304 on always-304 — http-cache-semantics gives a response without a status property the status 200 (index.js, CachePolicy constructor), so cacheable-request treats that 304 as storable — and revalidated it with the tag it carried on the next two fetches, handing the caller a 304 each time. On mismatch and no-validator-304 the 304 matched nothing stored (a different ETag; no validator against a stored Last-Modified), the policy rebuilt from the 304 is not storable, and cacheable-request deleted the entry (dist/index.js, cache.delete), so the third fetch was unconditional.",
        "gregjones/httpcache stores any response neither side marked no-store (httpcache.go, canStore), a cold 304 included, so on always-304 it revalidated that 304 with its tag on the next two fetches; and on a 304 it copies every header the 304 carries over the stored one (Transport.RoundTrip), so on mismatch its third fetch sent \"e-badhttp-2\" and got the 200 that restores the real tag.",
        "Six clients sent \"e-badhttp-2\" on their third fetch of mismatch — CacheControl, gregjones/httpcache, make-fetch-happen, requests-cache (cache_control=True), Squid, Apache Traffic Server (required_headers 0) — each because its 304 handling writes the 304's headers over the stored ones (cachecontrol/controller.py update_cached_response; requests_cache/policy/actions.py update_revalidated_response; make-fetch-happen's cache entry rewritten from the 304's headers; Squid's and Traffic Server's header merge on a 304). The server then answers 200 with the real tag, which is the 304/200/304 thrash the flavor describes. nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate) takes nothing from a 304's headers, sent the stored tag again and got a second 304.",
        "Apache Traffic Server (required_headers 0) strips a leading W/ from the ETag it revalidates with (HttpTransact.cc, issue_revalidate), so the weak flavor's tag went out as a strong-looking quoted tag; a 304 without an ETag costs it the stored ETag (merge_and_update_headers_for_cache_update deletes the cached ETag unless the 304 carries one), so its third fetch of no-validator-304 sent If-Modified-Since alone; and a Last-Modified that does not parse is not sent, so on bad-date, with no ETag either, its revalidation was an unconditional request. With its shipped default (required_headers 2: an explicit lifetime required) it stores none of these responses; the roster entry sets required_headers 0 and the row's configuration says so.",
        "Squid sends If-None-Match only for a strong, quoted ETag as stored (client_side_reply.cc, processExpired), so weak and unquoted were revalidated with If-Modified-Since alone; its If-Modified-Since is rebuilt from the stored entry's parsed Last-Modified, or from the entry's Date when there is no usable Last-Modified (changing, bad-date); and every request it forwarded, the cold one included, carried Cache-Control: max-age=259200, the largest freshness of its default refresh_pattern (http.cc, httpBuildRequestHeader).",
        "CacheControl stores a response only when it carries an ETag (or a max-age or Expires) — cachecontrol/controller.py, cache_response — so bad-date and future, whose responses have a Last-Modified and no ETag, were never stored and all three fetches were unconditional; the response's no-cache is not what decided.",
        "nginx proxy_cache (proxy_ignore_headers Cache-Control, 1 s validity, revalidate) stores a no-cache response only under proxy_ignore_headers Cache-Control (ngx_http_upstream.c, ngx_http_upstream_process_cache_control marks any no-cache, no-store or private value uncacheable; there is no other way), which is why the roster entry carries that directive and a one-second validity. When it revalidates, If-None-Match is the stored ETag byte for byte and If-Modified-Since the stored Last-Modified parsed and printed again, so an unparseable date (bad-date) sends no If-Modified-Since and, with no ETag, the revalidation was an unconditional request.",
        "Varnish ran its built-in VCL, whose vcl_beresp_control treats a Cache-Control containing no-cache, no-store or private as uncacheable and creates a hit-for-miss object, so every request went to the origin with no conditional header; and a 304 to a request it did not make conditional is a backend fetch failure to it (cache_fetch.c), answered with its own 503 — the one other-status row.",
        "Five clients sent the ISO 8601 Last-Modified back verbatim on bad-date and got the 304 that only an exact echo earns from this flavor: got + cacheable-request, gregjones/httpcache, hishel, make-fetch-happen, requests-cache (cache_control=True) — hishel's make_conditional_request copies the stored header, requests-cache's _update_validation_headers likewise, make-fetch-happen and got through http-cache-semantics' revalidationHeaders (which copies the stored strings and never parses the date), gregjones/httpcache through Set(\"if-modified-since\", lastModified). The four that build the date from a parsed time or a clock (curl -z, Squid, undici, GNU Wget -N) sent a different value and got the 200.",
        "0 of 140 rows hit the harness's cap and 0 failed to land. Every other row is a response this server sent, read back from the wire with its x-badhttp-version on every exchange."
      ],
      "freshness": "A dated capture, not a live measurement: taken 2026-10-12T19:44:05.856Z against badhttp version 0.24.0, with the client versions on each row. Cache behaviour changes between releases and this table does not update itself. Re-run it from scripts/etag-witness/ in the source and the numbers move; the date on every row is how you know whether to trust it.",
      "reproduce": "Start a fresh cache and a fresh working directory, ask the row's client to fetch the row's url three times in a row through its cache with no conditional header of your own, and compare each fetch's requests and what the caller received with the row. For the reference, fetch the same url with curl -sS --http1.1 and again with If-None-Match set to the ETag and If-Modified-Since to the Last-Modified it returned (scripts/witness-parse-etag.mjs carries the reference rules). Pace the calls against the zone limit of 100 per 10 s, and treat a response without x-badhttp-version as no observation."
    }
  },
  "common_fields": [
    "id",
    "corpus_id",
    "family",
    "flavor",
    "url",
    "client",
    "observed",
    "badhttp_version_observed",
    "status",
    "reported_error",
    "outcome",
    "license"
  ],
  "join": "corpus_id joins each row to a row of http://badhttp.dev/corpus.jsonl; family names the block above whose legend and fields apply",
  "reading_this": "In every family, `outcome` describes what the caller received and is never a verdict on the client. Each family block carries its own outcome_legend and its own reading_this, because what \"correct\" would even mean differs per flavor and is often unspecified. No client here is scored, ranked, or called conformant.",
  "license": "CC0-1.0",
  "license_url": "http://badhttp.dev/license"
}
