{"id":"truncate","family":"truncate","flavor":null,"layer":"message-framing","url":"https://badhttp.dev/truncate","method":"GET","request_headers":{},"defect":"Declares Content-Length: 1000 and delivers 500 bytes, then closes. An incomplete message: HTTP/1.1 sees a short read, HTTP/2 a stream reset.","specs":["RFC 9112 §6.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":true,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/truncate'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"drip","family":"drip","flavor":null,"layer":"message-framing","url":"https://badhttp.dev/drip","method":"GET","request_headers":{},"defect":"A chunked body trickled out over seconds. Tests read timeouts and streaming consumers.","specs":["RFC 9112 §7.1"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/drip'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"delay","family":"delay","flavor":null,"layer":"timing","url":"https://badhttp.dev/delay","method":"GET","request_headers":{},"defect":"Headers withheld for N seconds. Tests connect-vs-read timeout handling.","specs":[],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/delay'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"flaky","family":"flaky","flavor":null,"layer":"timing","url":"https://badhttp.dev/flaky","method":"GET","request_headers":{},"defect":"Fails a settable percentage of the time. Tests retry logic.","specs":[],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["randomness"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/flaky'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"headers","family":"headers","flavor":null,"layer":"diagnostic","url":"https://badhttp.dev/headers","method":"GET","request_headers":{},"defect":"Not a defect: reflects the request headers as this Worker sees them, which is how you discover what the edge changed on the way in.","specs":[],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["request-headers"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/headers'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"echo","family":"echo","flavor":null,"layer":"diagnostic","url":"https://badhttp.dev/echo","method":"GET","request_headers":{},"defect":"Not a defect: reflects the request body, capped at 16 KB.","specs":[],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["request-body"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/echo'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"status","family":"status","flavor":null,"layer":"status-codes","url":"https://badhttp.dev/status/{code}","method":"GET","request_headers":{},"defect":"Any status code, returned verbatim, with the headers that code requires (Allow on 405, WWW-Authenticate on 401, Retry-After on 503). Codes with no body per RFC 9110 get none. Unregistered codes in each class are served too, which is where clients that switch on a hard-coded set fall over.","specs":["RFC 9110 §15"],"parameter":{"name":"code","in":"path","registered_codes":[200,201,202,203,204,205,206,300,301,302,303,304,307,308,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,421,422,423,424,425,426,428,429,431,451,500,501,502,503,504,505,506,507,508,510,511],"range":"100-599"},"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/status/418'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.truncated","family":"badjson","flavor":"truncated","layer":"body-syntax","url":"https://badhttp.dev/badjson/truncated","method":"GET","request_headers":{},"defect":"Cut off mid-stream; Content-Length matches what was sent.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/truncated'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.trailing-comma","family":"badjson","flavor":"trailing-comma","layer":"body-syntax","url":"https://badhttp.dev/badjson/trailing-comma","method":"GET","request_headers":{},"defect":"Trailing comma. Valid JSON5, invalid JSON.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/trailing-comma'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.single-quotes","family":"badjson","flavor":"single-quotes","layer":"body-syntax","url":"https://badhttp.dev/badjson/single-quotes","method":"GET","request_headers":{},"defect":"Single-quoted strings. Python repr, not JSON.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/single-quotes'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.nan","family":"badjson","flavor":"nan","layer":"body-syntax","url":"https://badhttp.dev/badjson/nan","method":"GET","request_headers":{},"defect":"NaN and Infinity literals. Python json.dumps emits these by default.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/nan'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.bom","family":"badjson","flavor":"bom","layer":"body-syntax","url":"https://badhttp.dev/badjson/bom","method":"GET","request_headers":{},"defect":"UTF-8 byte order mark before the JSON. Some parsers choke.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/bom'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.html","family":"badjson","flavor":"html","layer":"body-syntax","url":"https://badhttp.dev/badjson/html","method":"GET","request_headers":{},"defect":"An HTML error page served with Content-Type: application/json and a 200.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/html'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.mislabeled","family":"badjson","flavor":"mislabeled","layer":"body-syntax","url":"https://badhttp.dev/badjson/mislabeled","method":"GET","request_headers":{},"defect":"Perfectly valid JSON served as text/html.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/mislabeled'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.empty","family":"badjson","flavor":"empty","layer":"body-syntax","url":"https://badhttp.dev/badjson/empty","method":"GET","request_headers":{},"defect":"Zero-byte body with Content-Type: application/json and a 200.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/empty'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.unterminated","family":"badjson","flavor":"unterminated","layer":"body-syntax","url":"https://badhttp.dev/badjson/unterminated","method":"GET","request_headers":{},"defect":"Unterminated string with an escaped quote inside.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/unterminated'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.bigint","family":"badjson","flavor":"bigint","layer":"body-syntax","url":"https://badhttp.dev/badjson/bigint","method":"GET","request_headers":{},"defect":"An integer above 2^53 and a number above double range. Precision loss or Infinity in most JS parsers.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/bigint'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.duplicate-keys","family":"badjson","flavor":"duplicate-keys","layer":"body-syntax","url":"https://badhttp.dev/badjson/duplicate-keys","method":"GET","request_headers":{},"defect":"Duplicate keys. Last-one-wins in most parsers, but not all.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/duplicate-keys'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.comments","family":"badjson","flavor":"comments","layer":"body-syntax","url":"https://badhttp.dev/badjson/comments","method":"GET","request_headers":{},"defect":"JSONC comments.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/comments'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.leading-garbage","family":"badjson","flavor":"leading-garbage","layer":"body-syntax","url":"https://badhttp.dev/badjson/leading-garbage","method":"GET","request_headers":{},"defect":"A stray line before the JSON document.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/leading-garbage'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.concatenated","family":"badjson","flavor":"concatenated","layer":"body-syntax","url":"https://badhttp.dev/badjson/concatenated","method":"GET","request_headers":{},"defect":"Two JSON documents back to back with no separator (not NDJSON, no newline).","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/concatenated'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.deep","family":"badjson","flavor":"deep","layer":"body-syntax","url":"https://badhttp.dev/badjson/deep","method":"GET","request_headers":{},"defect":"5,000 levels of nested arrays. Recursive parsers may blow the stack.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/deep'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"badjson.utf16","family":"badjson","flavor":"utf16","layer":"body-syntax","url":"https://badhttp.dev/badjson/utf16","method":"GET","request_headers":{},"defect":"Valid JSON, encoded as UTF-16LE with a BOM, labeled charset=utf-8.","specs":["RFC 8259","RFC 9110 §8.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":[],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/badjson/utf16'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.ok","family":"sse","flavor":"ok","layer":"event-streams","url":"https://badhttp.dev/sse/ok","method":"GET","request_headers":{},"defect":"A correct stream, for comparison: retry, id, event and data fields, blank-line delimited, then a clean close. ?events= and ?interval= (ms).","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/ok'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.stall","family":"sse","flavor":"stall","layer":"event-streams","url":"https://badhttp.dev/sse/stall","method":"GET","request_headers":{},"defect":"Headers and one event arrive, then nothing for ?seconds= (default 10, max 20), then a clean close. A client with a connect timeout but no read timeout waits here.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/stall'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.cut","family":"sse","flavor":"cut","layer":"event-streams","url":"https://badhttp.dev/sse/cut","method":"GET","request_headers":{},"defect":"Ends mid-event with a clean close: a complete event, then \"data: {\\\"partial\\\":tr\" and EOF with no blank line. The spec says discard it; many parsers emit it or leak it into the next connection.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/cut'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.drop","family":"sse","flavor":"drop","layer":"event-streams","url":"https://badhttp.dev/sse/drop","method":"GET","request_headers":{},"defect":"The connection is reset mid-event (HTTP/1.1: closed with bytes outstanding; HTTP/2: RST_STREAM). Your client should report an error, not a clean end. Carries a Content-Length so the runtime can reset for real.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":true,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/drop'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.crlf","family":"sse","flavor":"crlf","layer":"event-streams","url":"https://badhttp.dev/sse/crlf","method":"GET","request_headers":{},"defect":"Every line ends in CRLF. The spec allows CR, LF or CRLF; parsers that split on \"\\n\" leave a trailing \"\\r\" in every value.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/crlf'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.cr","family":"sse","flavor":"cr","layer":"event-streams","url":"https://badhttp.dev/sse/cr","method":"GET","request_headers":{},"defect":"Every line ends in a bare CR. Spec-legal. Almost nobody handles it.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/cr'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.no-space","family":"sse","flavor":"no-space","layer":"event-streams","url":"https://badhttp.dev/sse/no-space","method":"GET","request_headers":{},"defect":"Field values with no space after the colon, two spaces, and nothing at all. The spec strips exactly one leading space: \"data:foo\" is \"foo\", \"data:  foo\" is \" foo\".","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/no-space'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.multiline","family":"sse","flavor":"multiline","layer":"event-streams","url":"https://badhttp.dev/sse/multiline","method":"GET","request_headers":{},"defect":"Multiple data: lines per event (joined with \"\\n\"), a colon inside a value, an empty data: line in the middle. Parsers that keep only the last line, or split on \":\", fail here.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/multiline'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.comments","family":"sse","flavor":"comments","layer":"event-streams","url":"https://badhttp.dev/sse/comments","method":"GET","request_headers":{},"defect":"A leading BOM, \": keepalive\" comment lines, unknown fields (\"foo: bar\") and a field line with no colon. All four must be ignored; you should see exactly three events.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/comments'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.split-utf8","family":"sse","flavor":"split-utf8","layer":"event-streams","url":"https://badhttp.dev/sse/split-utf8","method":"GET","request_headers":{},"defect":"Multi-byte UTF-8 characters split across chunk boundaries (a 4-byte emoji as 2+2, a 3-byte euro sign as 1+2). A client that decodes each chunk separately sees U+FFFD.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/split-utf8'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.wrong-type","family":"sse","flavor":"wrong-type","layer":"event-streams","url":"https://badhttp.dev/sse/wrong-type","method":"GET","request_headers":{},"defect":"A valid stream served as text/plain. A browser EventSource must fail; does your client notice?","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/wrong-type'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.error-event","family":"sse","flavor":"error-event","layer":"event-streams","url":"https://badhttp.dev/sse/error-event","method":"GET","request_headers":{},"defect":"An event whose name is \"error\". EventSource routes it to onerror beside real transport failures; does your client tell them apart?","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/error-event'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.big","family":"sse","flavor":"big","layer":"event-streams","url":"https://badhttp.dev/sse/big","method":"GET","request_headers":{},"defect":"One event with a ?bytes= (default 64 KiB, max 1 MiB) data line. Line buffers with a fixed cap truncate or crash.","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/big'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"sse.resume","family":"sse","flavor":"resume","layer":"event-streams","url":"https://badhttp.dev/sse/resume","method":"GET","request_headers":{},"defect":"Sends events 1–3 and closes. Reconnect with Last-Event-ID: 3 and it sends 4–6; with 6 it answers 204 (stop). Its retry is 1000 ms so the three requests complete quickly. Does your client send Last-Event-ID on reconnect, and stop on a 204?","specs":["WHATWG HTML §9.2 (Server-Sent Events)"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["last-event-id","timing"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/sse/resume'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.ok","family":"range","flavor":"ok","layer":"range-requests","url":"https://badhttp.dev/range/ok","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"The control, fully correct: Accept-Ranges, strong ETag, Last-Modified; single, suffix, open-ended and multiple ranges (multipart/byteranges), 416 with \"bytes */length\" when unsatisfiable, If-Range honored (strong validators only), and the conditional headers evaluated as RFC 9110 says. HEAD ignores Range, as the spec requires. No Range gets a 200.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/ok'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.ignore","family":"range","flavor":"ignore","layer":"range-requests","url":"https://badhttp.dev/range/ignore","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"No range support at all: no Accept-Ranges header, and every request gets a 200 with the full body. Legal — range support is optional — and the #1 real-world case: a resuming client must notice the 200 and start over, not append.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/ignore'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.advertise-only","family":"range","flavor":"advertise-only","layer":"range-requests","url":"https://badhttp.dev/range/advertise-only","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"Advertises Accept-Ranges: bytes on every response, then ignores every Range header and sends 200 with the full body. Aimed at segmented downloaders (aria2 and friends) that split into N connections because of the advertisement — and then receive N full bodies.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/advertise-only'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.off-by-one","family":"range","flavor":"off-by-one","layer":"range-requests","url":"https://badhttp.dev/range/off-by-one","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"Treats the range end as exclusive: bytes=a-b gets bytes a..b-1, one short, while Content-Range still claims a-b. Content-Length matches the short body, so the two headers disagree — the classic fencepost, one missing byte per segment. The lie is applied to the first range; extra ranges are ignored.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/off-by-one'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.shifted","family":"range","flavor":"shifted","layer":"range-requests","url":"https://badhttp.dev/range/shifted","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"Serves bytes a+1..b+1 while Content-Range claims a-b. The envelope looks right; every byte is wrong — detectable only because the body is self-describing (the offsets inside the file will not match where you put them). At the end of the document the shifted window is clamped, so the final segment also runs one byte short. First range only.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/shifted'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.suffix-as-prefix","family":"range","flavor":"suffix-as-prefix","layer":"range-requests","url":"https://badhttp.dev/range/suffix-as-prefix","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"The naive suffix-range bug: bytes=-n is served as the FIRST n bytes of the document while Content-Range claims the last n. A client resuming \"the tail\" appends the head — silent corruption on exactly the request curl sends for a suffix. The lie applies when the first range is a suffix; any other request is served correctly, multipart included.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/suffix-as-prefix'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.from-zero","family":"range","flavor":"from-zero","layer":"range-requests","url":"https://badhttp.dev/range/from-zero","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"Acknowledges your range with a 206 — then serves the whole document from byte zero, with an honest Content-Range: bytes 0-{length-1}/{length} that simply disagrees with what you asked. A client that appends without checking Content-Range against its request builds a file with a duplicated prefix.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/from-zero'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.wrong-total","family":"range","flavor":"wrong-total","layer":"range-requests","url":"https://badhttp.dev/range/wrong-total","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"Correct bytes, but Content-Range lies about the total: bytes a-b/{2×length}. Asking for bytes past the real end gets 416 with the same inflated total, so a download loop that trusts it never finishes. First range only.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/wrong-total'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.no-content-range","family":"range","flavor":"no-content-range","layer":"range-requests","url":"https://badhttp.dev/range/no-content-range","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"A single-range 206 with the right bytes and no Content-Range header (a violation of RFC 9110 §15.3.7). What offset does your client think this is? First range only.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/no-content-range'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.always-206","family":"range","flavor":"always-206","layer":"range-requests","url":"https://badhttp.dev/range/always-206","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"A request with no Range header still gets a 206 (Content-Range: bytes 0-{length-1}/{length}, full body). With a valid Range it behaves correctly; an ignored Range (malformed, or over the caps) is treated as absent, so it also gets the full-body 206. Some CDNs and proxies really do this.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/always-206'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.200-content-range","family":"range","flavor":"200-content-range","layer":"range-requests","url":"https://badhttp.dev/range/200-content-range","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"Honors the range — right bytes, right Content-Range header — but the status is 200. A contradiction: which does your client believe, the status or the header? First range only.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/200-content-range'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.always-416","family":"range","flavor":"always-416","layer":"range-requests","url":"https://badhttp.dev/range/always-416","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"Every Range request gets 416 with Content-Range: bytes */{length}; without Range, a 200. Tests give-up-and-restart logic.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/always-416'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.unknown-total","family":"range","flavor":"unknown-total","layer":"range-requests","url":"https://badhttp.dev/range/unknown-total","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"Correct 206, but Content-Range says bytes a-b/* — total unknown, which is legal. Preallocation and progress logic that requires the total breaks. First range only.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/unknown-total'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"range.if-range-ignored","family":"range","flavor":"if-range-ignored","layer":"range-requests","url":"https://badhttp.dev/range/if-range-ignored","method":"GET","request_headers":{"range":"bytes=0-99"},"defect":"The resource changes on every request (a generation stamp appears in the ETag and in every line of the body) and If-Range is ignored: a stale validator still gets a 206 from the new generation, where a correct server would send the full 200. Resume across it and your file mixes generations — run grep -oE \"g[0-9a-f]{16}\" file | sort -u on it: more than one value is the corruption. Nondeterministic by design.","specs":["RFC 9110 §14"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["range","if-range"],"curl":"curl --http1.1 --raw -sS -D - -H 'range: bytes=0-99' --output - 'https://badhttp.dev/range/if-range-ignored'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.ok","family":"etag","flavor":"ok","layer":"conditional-requests","url":"https://badhttp.dev/etag/ok","method":"GET","request_headers":{},"defect":"The control, fully correct: strong ETag, Last-Modified, cache-control: no-cache. If-Match (strong compare; * passes), If-Unmodified-Since, If-None-Match (weak compare, lists and * supported; match is 304), If-Modified-Since (ignored when If-None-Match is present, and ignored unless it is a valid HTTP-date). The 304 carries the ETag.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/ok'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.weak","family":"etag","flavor":"weak","layer":"conditional-requests","url":"https://badhttp.dev/etag/weak","method":"GET","request_headers":{},"defect":"The only validator is weak: W/\"…\". If-None-Match uses weak comparison, so revalidation works (304). If-Match requires strong comparison and a weak validator never strong-matches, so every If-Match gets 412 — except If-Match: *, which passes. Catches clients that treat W/ as part of the value.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/weak'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.changing","family":"etag","flavor":"changing","layer":"conditional-requests","url":"https://badhttp.dev/etag/changing","method":"GET","request_headers":{},"defect":"A different strong ETag on every response, and no Last-Modified (a changing resource with a frozen date would be a second lie). If-None-Match never matches, so a cache revalidates forever and re-downloads every time: thrash. Nondeterministic by design.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/changing'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.ignore","family":"etag","flavor":"ignore","layer":"conditional-requests","url":"https://badhttp.dev/etag/ignore","method":"GET","request_headers":{},"defect":"Sends a perfectly good ETag and Last-Modified, then ignores every conditional header: always 200, full body. (Violates a MUST. That is the point.) Your cache keeps asking; it keeps not listening.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/ignore'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.always-304","family":"etag","flavor":"always-304","layer":"conditional-requests","url":"https://badhttp.dev/etag/always-304","method":"GET","request_headers":{},"defect":"Every GET is answered 304 — even the first, with no conditional headers at all. A cold cache is told \"you already have it\" about a body it has never seen. Broken proxies really do this.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/always-304'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.mismatch","family":"etag","flavor":"mismatch","layer":"conditional-requests","url":"https://badhttp.dev/etag/mismatch","method":"GET","request_headers":{},"defect":"Revalidation \"succeeds\" — If-None-Match matches, 304 — but the 304 carries a different ETag than the one you sent. A cache that adopts it misses on its next revalidation (200, real validator restored) and then matches again: a permanent 304/200/304 thrash.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/mismatch'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.no-validator-304","family":"etag","flavor":"no-validator-304","layer":"conditional-requests","url":"https://badhttp.dev/etag/no-validator-304","method":"GET","request_headers":{},"defect":"If-None-Match matches and the 304 comes back bare: no ETag, no Last-Modified. A violation — RFC 9110 §15.4.5 says the 304 MUST carry the ETag its 200 would have — and hostile to caches that need the validator to know which stored response was confirmed.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/no-validator-304'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.unquoted","family":"etag","flavor":"unquoted","layer":"conditional-requests","url":"https://badhttp.dev/etag/unquoted","method":"GET","request_headers":{},"defect":"The ETag header is a bare token with no quotes (spec-invalid, common in the wild). The server matches If-None-Match sloppily — quoted, bare, weak-prefixed, anything goes — and If-Modified-Since works normally. What does your client send back, and does its parser cope?","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/unquoted'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.bad-date","family":"etag","flavor":"bad-date","layer":"conditional-requests","url":"https://badhttp.dev/etag/bad-date","method":"GET","request_headers":{},"defect":"No ETag; Last-Modified is ISO 8601, not an HTTP-date (invalid). Revalidation is by exact string comparison of If-Modified-Since against that value — what a naive server does. Only a client that echoes the header back verbatim ever gets its 304; one that parses and reformats, or discards the unparseable date, refetches forever.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/bad-date'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"etag.future","family":"etag","flavor":"future","layer":"conditional-requests","url":"https://badhttp.dev/etag/future","method":"GET","request_headers":{},"defect":"No ETag; Last-Modified is one year from today (a valid HTTP-date that is always in the future, moving at midnight UTC). The date comparison itself is honest, so a client that echoes today's header back verbatim still gets 304 — until the date rolls — while one that sends its own clock always gets 200. Which is yours? Nondeterministic across days by design.","specs":["RFC 9110 §8.8","RFC 9110 §13","RFC 9111"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["if-none-match","if-modified-since","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/etag/future'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.ok","family":"cookies","flavor":"ok","layer":"cookies","url":"https://badhttp.dev/cookies/ok","method":"GET","request_headers":{},"defect":"The control, fully correct and minimal: badhttp_ok=1; Path=/cookies; Max-Age=3600. Store it, return it to /cookies/* for an hour.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/ok'","expect":"The cookie is stored and comes back on every /cookies/* request for an hour.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.echo","family":"cookies","flavor":"echo","layer":"cookies","url":"https://badhttp.dev/cookies/echo","method":"GET","request_headers":{},"defect":"The readback. Sets nothing; returns the Cookie header you sent (raw, plus base64 of its UTF-8 re-encoding) and the parsed pairs in order, duplicates preserved. Three notes: an upstream hop joins multiple Cookie header lines with \"; \" before the Worker sees them; the runtime replaces bytes that are not valid UTF-8 with U+FFFD — so EF BF BD in the base64 means your client sent raw non-UTF-8 bytes; and a Cookie header over 8,199 bytes is silently dropped upstream of the Worker (observed live: 8,199 arrives intact, 8,200 never arrives, the request otherwise succeeds).","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/echo'","expect":"Whatever your client sent, as delivered to the Worker.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.folded","family":"cookies","flavor":"folded","layer":"cookies","url":"https://badhttp.dev/cookies/folded","method":"GET","request_headers":{},"defect":"Two cookies folded into ONE Set-Cookie header, comma-separated: \"badhttp_folded_a=1, badhttp_folded_b=2\". bis §3 forbids folding Set-Cookie (RFC 6265 had it at SHOULD NOT); the parse algorithm yields ONE cookie whose value is \"1, badhttp_folded_b=2\". A client that splits on commas invents a second cookie.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/folded'","expect":"One stored cookie, badhttp_folded_a, with the comma and the fake second cookie inside its value.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.many","family":"cookies","flavor":"many","layer":"cookies","url":"https://badhttp.dev/cookies/many","method":"GET","request_headers":{},"defect":"?count= separate Set-Cookie headers (1-20, default 10), badhttp_many_01 onward, zero-padded. Tests per-response cookie handling and ordering.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/many'","expect":"All stored and all sent back. §5.4's sort (longer path, then earlier creation) is a SHOULD: expect creation order, but an unsorted jar is still conformant.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.duplicate","family":"cookies","flavor":"duplicate","layer":"cookies","url":"https://badhttp.dev/cookies/duplicate","method":"GET","request_headers":{},"defect":"The same name twice with different paths: badhttp_dup=deep; Path=/cookies/echo and badhttp_dup=shallow; Path=/cookies. Two distinct cookies. A jar keyed on name alone silently loses one.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/duplicate'","expect":"At /cookies/echo the Cookie header carries both — longer path first per §5.4's SHOULD-order: badhttp_dup=deep; badhttp_dup=shallow.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.on-redirect","family":"cookies","flavor":"on-redirect","layer":"cookies","url":"https://badhttp.dev/cookies/on-redirect","method":"GET","request_headers":{},"defect":"A 302 to /cookies/echo that carries Set-Cookie: badhttp_redirect=1 ON the redirect itself. A historic bug class: clients that drop Set-Cookie on 3xx responses. One shot: curl -sL -c jar -b jar.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/on-redirect'","expect":"The cookie from the 302 is stored and presented on the follow-up to /cookies/echo.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.delete","family":"cookies","flavor":"delete","layer":"cookies","url":"https://badhttp.dev/cookies/delete","method":"GET","request_headers":{},"defect":"The cleanup: one expiring Set-Cookie for every cookie this family can plant, each with the exact Path (and Domain, and prefix-required attributes) it was set with — RFC 6265 §5.3 removes a cookie only on a name+domain+path match, so a deletion that is casual about attributes deletes nothing. Uses both idioms: Expires in 1970 and Max-Age=0.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/delete'","expect":"A jar that visited every flavor is empty again (a correct jar simply rejects the deletions for cookies it refused to store in the first place).","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.conflicting-expiry","family":"cookies","flavor":"conflicting-expiry","layer":"cookies","url":"https://badhttp.dev/cookies/conflicting-expiry","method":"GET","request_headers":{},"defect":"badhttp_conflict=alive with BOTH Expires in 1970 AND Max-Age=3600. §5.3 step 3 consults Max-Age before Expires (prose in §4.1.2.2: Max-Age has precedence). A client honoring Expires deletes a cookie that should live an hour.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/conflicting-expiry'","expect":"The cookie lives: Max-Age beats Expires.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.bad-expires","family":"cookies","flavor":"bad-expires","layer":"cookies","url":"https://badhttp.dev/cookies/bad-expires","method":"GET","request_headers":{},"defect":"Expires in ISO 8601 (2027-08-23T12:00:00Z), which the cookie-date algorithm (§5.1.1) cannot parse — \"-\" is a delimiter and no month token survives. The attribute is ignored and the cookie becomes a session cookie. A homegrown jar that feeds Expires to a general date parser mints a 2027 expiry instead; /cookies/delete clears it either way.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/bad-expires'","expect":"A session cookie: stored, sent back, gone when the jar session ends.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.far-future","family":"cookies","flavor":"far-future","layer":"cookies","url":"https://badhttp.dev/cookies/far-future","method":"GET","request_headers":{},"defect":"Expires in the year 9999 (Fri, 01 Jan 9999 00:00:00 GMT). bis §5.5 says user agents SHOULD cap cookie lifetime (400 days recommended); CLI jars mostly predate the cap. /cookies/delete removes it.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/far-future'","expect":"Stored either way; a client with the bis cap clamps the expiry, one without keeps the 9999 date.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.wrong-domain","family":"cookies","flavor":"wrong-domain","layer":"cookies","url":"https://badhttp.dev/cookies/wrong-domain","method":"GET","request_headers":{},"defect":"Domain=example.com on a cookie set by this host. The Domain does not domain-match the request host, so the whole cookie MUST be ignored (§5.3 step 6). Jar-observable: it must simply never appear. Max-Age is 300 s so a jar that wrongly keeps it is only polluted briefly.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/wrong-domain'","expect":"Nothing is stored. If your jar has badhttp_wrong_domain, that is the bug.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.public-suffix","family":"cookies","flavor":"public-suffix","layer":"cookies","url":"https://badhttp.dev/cookies/public-suffix","method":"GET","request_headers":{},"defect":"Domain=dev — a public suffix. A cookie scoped to a whole TLD is a supercookie; a jar configured with a public-suffix list ignores it (§5.3 step 5 — conditional on that configuration; plain RFC 6265 without a PSL would accept it, since badhttp.dev domain-matches dev). On this host the single-label Domain also trips the older no-embedded-dot heuristic, so PSL-free jars reject it too; only a jar with neither guard stores it — and would then send it back here, so /cookies/echo can catch it. Max-Age 300 s bounds the damage.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/public-suffix'","expect":"Nothing is stored; badhttp_supercookie in any Cookie header is the bug.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.domain","family":"cookies","flavor":"domain","layer":"cookies","url":"https://badhttp.dev/cookies/domain","method":"GET","request_headers":{},"defect":"The accepted-Domain pair: badhttp_domain_dot with Domain=.<this host> (leading dot) and badhttp_domain with Domain=<this host>. §5.2.3 strips the leading %x2E, so both become identical domain cookies (host-only flag off) — a classic divergence between jar generations and jar file formats. Meaningful on badhttp.dev itself.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/domain'","expect":"Both stored and both sent back; the jar file shows whether your client recorded them as domain cookies (curl writes a leading .badhttp.dev) and that the dot made no difference.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.path-prefix","family":"cookies","flavor":"path-prefix","layer":"cookies","url":"https://badhttp.dev/cookies/path-prefix","method":"GET","request_headers":{},"defect":"Path=/cookie — one letter short of /cookies. Path-matching (§5.1.4) requires the prefix to end at a \"/\" boundary, so this cookie must NEVER be sent to /cookies/*. A naive prefix-matcher sends it anyway.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/path-prefix'","expect":"Stored, but never sent back here. badhttp_path_prefix at /cookies/echo is the bug.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.name-prefixes","family":"cookies","flavor":"name-prefixes","layer":"cookies","url":"https://badhttp.dev/cookies/name-prefixes","method":"GET","request_headers":{},"defect":"Four prefixed cookies (bis §4.1.3, §5.4 — the prefixes do not exist in RFC 6265): __Host-badhttp_good (valid: Path=/, Secure, no Domain), __Host-badhttp_bad (invalid: Path is not /), __Secure-badhttp_good (valid: Secure), __Secure-badhttp_bad (invalid: no Secure). A bis client stores exactly the two _good ones; an RFC-6265-only jar conformantly stores all four. Meaningful over HTTPS only.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/name-prefixes'","expect":"A current client keeps the two _good cookies and rejects the two _bad; a pre-bis jar keeps all four.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.quoted","family":"cookies","flavor":"quoted","layer":"cookies","url":"https://badhttp.dev/cookies/quoted","method":"GET","request_headers":{},"defect":"badhttp_quoted=\"hello world\" (a DQUOTE-wrapped value with a space) and badhttp_semi=\"semi;colon\" (a semicolon inside the quotes — but the parser splits on \";\" before it ever sees quotes, so the stored value is \"semi with an unclosed quote). What does your client send back — quotes kept, stripped, re-added?","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/quoted'","expect":"badhttp_quoted comes back with the quotes and the space; badhttp_semi comes back as \"semi (quote open, nothing else).","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.utf8","family":"cookies","flavor":"utf8","layer":"cookies","url":"https://badhttp.dev/cookies/utf8","method":"GET","request_headers":{},"defect":"A value of raw UTF-8: badhttp_utf8=☃ (the bytes e2 98 83 on the wire; the runtime UTF-8-encodes header strings, which is itself a platform quirk worth knowing). Outside the cookie-octet grammar; real servers do it anyway. Jars differ: store raw, percent-encode, or drop. The echo base64 field shows exactly what came back.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/utf8'","expect":"Whatever your jar did with the bytes, the echo shows it.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.nameless","family":"cookies","flavor":"nameless","layer":"cookies","url":"https://badhttp.dev/cookies/nameless","method":"GET","request_headers":{},"defect":"Two nameless shapes at different paths so both can coexist: a Set-Cookie with no \"=\" at all (badhttp-just-a-value, default path /cookies) and one that starts with \"=\" (=badhttp_empty_name; Path=/cookies/echo). RFC 6265 §5.2 ignores both — step 2 (no \"=\") and step 5 (empty name); the bis parse stores each as a value with an empty name. Generations of jars really do differ here.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/nameless'","expect":"Spec-dependent: an RFC 6265 jar stores neither; a bis-style jar stores both as nameless values and echoes them bare.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"cookies.huge","family":"cookies","flavor":"huge","layer":"cookies","url":"https://badhttp.dev/cookies/huge","method":"GET","request_headers":{},"defect":"One cookie whose name plus value sum to exactly ?bytes= bytes (64-8192, default 4096), padded with x. bis §5.6 step 5 says a client MUST ignore the cookie when name+value exceed 4096 bytes (RFC 6265 §6.1 has only a SHOULD-support floor, measured including attributes). So 4096 survives a conformant jar and 4097 must not. The echo round trip tells you your client's cap.","specs":["RFC 6265","draft-ietf-httpbis-rfc6265bis"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/cookies/huge'","expect":"At 4096 the cookie survives a bis-conformant jar; at 4097 it must be ignored — and pre-bis jars have their own caps.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.basic","family":"auth","flavor":"basic","layer":"authentication","url":"https://badhttp.dev/auth/basic","method":"GET","request_headers":{},"defect":"The control, RFC 7617 done right: 401 with WWW-Authenticate: Basic realm=\"badhttp\", charset=\"UTF-8\" until you send agent:correct. Wrong credentials get a fresh challenge; a value that does not decode (bad base64, no colon) gets a 401 whose body names the exact defect.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/basic'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.bearer","family":"auth","flavor":"bearer","layer":"authentication","url":"https://badhttp.dev/auth/bearer","method":"GET","request_headers":{},"defect":"The control, RFC 6750 done right: a bare Bearer challenge (no error param) until credentials arrive. badhttp-token-ok is a 200; an unknown token is 401 error=\"invalid_token\"; a value that is not token68-shaped is 400 error=\"invalid_request\"; badhttp-token-limited is 403 error=\"insufficient_scope\", scope=\"badhttp:full\" — and that 403 carries the challenge, unlike /auth/forbidden.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/bearer'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.digest","family":"auth","flavor":"digest","layer":"authentication","url":"https://badhttp.dev/auth/digest","method":"GET","request_headers":{},"defect":"The control, RFC 7616 with algorithm=MD5, qop=\"auth\": full validation (username, realm, uri against the request-target, nonce, response hash, cnonce and nc required), stale=true when the nonce ages out (5–10 min), Authentication-Info with rspauth on success. The nonce is a deterministic time bucket, which trades RFC-advised uniqueness for statelessness — nothing here is protected, so replay is a non-issue. MD5 is for interop testing, not an endorsement.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/digest'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.digest-sha256","family":"auth","flavor":"digest-sha256","layer":"authentication","url":"https://badhttp.dev/auth/digest-sha256","method":"GET","request_headers":{},"defect":"The same correct Digest with algorithm=SHA-256 (RFC 7616's preferred). Some clients only speak MD5 and fail here — how loudly is the test.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/digest-sha256'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.none","family":"auth","flavor":"none","layer":"authentication","url":"https://badhttp.dev/auth/none","method":"GET","request_headers":{},"defect":"A 401 with no WWW-Authenticate header at all — violates a MUST (RFC 9110 §15.5.2), rampant in real APIs. There is no challenge to answer, so only a client that sends Basic agent:correct preemptively, unprompted, ever gets its 200: this flavor is the preemptive-auth witness.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/none'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.bare-scheme","family":"auth","flavor":"bare-scheme","layer":"authentication","url":"https://badhttp.dev/auth/bare-scheme","method":"GET","request_headers":{},"defect":"WWW-Authenticate: Basic — no realm, which RFC 7617 requires. Sends agent:correct anyway? It works. What does your client make of a challenge with no parameters at all?","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/bare-scheme'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.unknown-scheme","family":"auth","flavor":"unknown-scheme","layer":"authentication","url":"https://badhttp.dev/auth/unknown-scheme","method":"GET","request_headers":{},"defect":"A challenge in a scheme nobody speaks: X-Badhttp-Frobnicate realm=\"badhttp\". Every request is 401. A good client fails cleanly and does not loop; it certainly does not crash.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/unknown-scheme'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.token68","family":"auth","flavor":"token68","layer":"authentication","url":"https://badhttp.dev/auth/token68","method":"GET","request_headers":{},"defect":"One header, two challenges, and the first ends in a token68 (X-Badhttp-Opaque dG9rZW42OA==, Basic realm=\"badhttp\") — legal per RFC 9110's ABNF and harder on comma-naive parsers than /auth/multi, because the first challenge has no name=value shape at all. Valid Basic credentials work.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/token68'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.multi","family":"auth","flavor":"multi","layer":"authentication","url":"https://badhttp.dev/auth/multi","method":"GET","request_headers":{},"defect":"One header, two challenges: Digest (realm=\"badhttp\", qop=\"auth\", algorithm=MD5), then Basic realm=\"badhttp\" — the comma-separated challenge list that breaks parsers which split on commas, since parameters and challenges share the delimiter. Either valid Basic or valid Digest works; the body says which the server matched.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/multi'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.case","family":"auth","flavor":"case","layer":"authentication","url":"https://badhttp.dev/auth/case","method":"GET","request_headers":{},"defect":"The challenge arrives as bASIc rEALM=\"badhttp\". Scheme names and parameter names are case-insensitive (RFC 9110 §11.1); agent:correct works — if your client recognized the challenge at all.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/case'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.quoted","family":"auth","flavor":"quoted","layer":"authentication","url":"https://badhttp.dev/auth/quoted","method":"GET","request_headers":{},"defect":"The realm is \"badhttp says \\\"hello\\\", agent\" — escaped quotes and a comma inside the quoted string (and it still names badhttp, the one place a browser might display it). A parser that splits on commas before honoring quotes sees two garbage challenges. agent:correct works.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/quoted'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.utf8","family":"auth","flavor":"utf8","layer":"authentication","url":"https://badhttp.dev/auth/utf8","method":"GET","request_headers":{},"defect":"Basic realm=\"badhttp-utf8\", charset=\"UTF-8\", credentials agent / sésame. The é forces an encoding choice, and charset is purely advisory (RFC 7617 §2.1), so both the UTF-8 and the Latin-1 encoding are accepted and the 200 reports which one your client sent (encoding: \"utf-8\" or \"latin1\"). A witness instrument, not a gate.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/utf8'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.always-401","family":"auth","flavor":"always-401","layer":"authentication","url":"https://badhttp.dev/auth/always-401","method":"GET","request_headers":{},"defect":"A perfect Basic challenge that rejects everything — agent:correct included (an x-badhttp-warning header says so). The retry-loop trap: how many times does your client try before giving up?","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/always-401'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.accept-any","family":"auth","flavor":"accept-any","layer":"authentication","url":"https://badhttp.dev/auth/accept-any","method":"GET","request_headers":{},"defect":"The opposite trap: any nonempty Authorization header is a 200 with authenticated:true and checked:false — the middleware bug that checks presence, not validity. The body names the scheme only when it is one the server knows (Basic, Bearer, Digest), never anything else you sent. If you saw authenticated:true here without configuring the documented test credentials, your client just leaked ambient credentials to a server that accepts anything — treat them as exposed.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/accept-any'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.forbidden","family":"auth","flavor":"forbidden","layer":"authentication","url":"https://badhttp.dev/auth/forbidden","method":"GET","request_headers":{},"defect":"agent:correct authenticates — and gets 403, with no WWW-Authenticate on it: authenticated is not authorized, and a client SHOULD NOT auto-retry a 403 (RFC 9110). Compare /auth/bearer's insufficient_scope 403, which does carry a challenge.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/forbidden'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.stale","family":"auth","flavor":"stale","layer":"authentication","url":"https://badhttp.dev/auth/stale","method":"GET","request_headers":{},"defect":"The Digest stale dance, deterministic: the first challenge's nonce is generation 1; a VALID response over it gets 401 with stale=true and a generation-2 nonce (stale=true promises the credentials were right — a client that honors it retries without prompting); a valid response over generation 2 is the 200. A wrong password gets a plain 401, never stale.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/stale'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.proxy","family":"auth","flavor":"proxy","layer":"authentication","url":"https://badhttp.dev/auth/proxy","method":"GET","request_headers":{},"defect":"An origin server demanding proxy authentication: 407 with Proxy-Authenticate: Basic realm=\"badhttp-proxy\" from a host that is not your proxy. Proxy-Authorization with agent:correct works. A client that answered this automatically just revealed it would leak its proxy credentials to any origin that asks.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/proxy'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"auth.redirect","family":"auth","flavor":"redirect","layer":"authentication","url":"https://badhttp.dev/auth/redirect","method":"GET","request_headers":{},"defect":"A 302 to /auth/basic. The question is what your client does with credentials across the hop: does the Authorization it was about to send (or was sent here with) follow to the redirect target? Same host, so this is the benign half of the cross-origin credential-leak class — the observable is whether auth survives a redirect at all.","specs":["RFC 9110 §11","RFC 7617","RFC 6750","RFC 7616"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["authorization","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/auth/redirect'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.ok","family":"compress","flavor":"ok","layer":"content-coding","url":"https://badhttp.dev/compress/ok","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"The control: gzip if the set the edge reports for you lists gzip, identity otherwise (that set reads no header, \"*\" and an empty value as nothing); Vary: Accept-Encoding; a correct Content-Length as sent; and the plaintext's length and SHA-256 in x-badhttp-plain-bytes / x-badhttp-plain-sha256 so you can prove what you decoded. q-values are already dropped from that set, so gzip;q=0 still gets gzip and identity;q=0 cannot produce a 406 from here; x-badhttp-negotiated-from shows the set.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/ok'","edge_transcoding":"Nothing to transcode: a client that did not list gzip is sent identity by this Worker itself — unless it refused gzip with q=0, which the edge reports as listing it, and then it gets gzip.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.br","family":"compress","flavor":"br","layer":"content-coding","url":"https://badhttp.dev/compress/br","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Brotli (RFC 7932), pre-computed for the default 4096-byte document because this runtime has no brotli encoder; ?length= is accepted only at 4096.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/br'","edge_transcoding":"Delivered only to a client whose Accept-Encoding lists br; decoded to identity for everyone else, Content-Encoding removed and Content-Length dropped.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.zstd","family":"compress","flavor":"zstd","layer":"content-coding","url":"https://badhttp.dev/compress/zstd","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Zstandard (RFC 8878), the newest coding browsers advertise and one several SDKs still cannot decode. Pre-computed for the default 4096-byte document; ?length= is accepted only at 4096.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/zstd'","edge_transcoding":"Delivered only to a client whose Accept-Encoding lists zstd (the edge honours that even though it drops zstd from the set it reports to this Worker); decoded to identity for everyone else.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.deflate","family":"compress","flavor":"deflate","layer":"content-coding","url":"https://badhttp.dev/compress/deflate","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Content-Encoding: deflate done right: a zlib stream (RFC 1950 around RFC 1951), which is what the coding name means. No client probed on 2026-09-01 received it through this host — Cloudflare's edge, the CDN in front of this Worker, decoded it for every one of them, even one that asked for it (GET /compress → edge_transcoding has the per-flavor list) — and the flavor exists so that observation stays reproducible.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/deflate'","edge_transcoding":"Decoded to identity for every client probed on 2026-09-01, including one that sent Accept-Encoding: deflate — the only coding among those probed that no client received.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.not-compressed","family":"compress","flavor":"not-compressed","layer":"content-coding","url":"https://badhttp.dev/compress/not-compressed","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Content-Encoding: gzip on a body that is not compressed — plain text, the case most often reported in the wild (a proxy or framework that sets the header without compressing). A decoder that trusts the header fails on the first byte; some clients fall back to the raw body, some do not.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/not-compressed'","edge_transcoding":"Cannot decode it, so it removes the header and delivers the text: a client without gzip sees a correct response.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.undeclared","family":"compress","flavor":"undeclared","layer":"content-coding","url":"https://badhttp.dev/compress/undeclared","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"The mirror image: the body IS gzip (starts 1f 8b) but there is no Content-Encoding header at all — text/plain says it is text. A client that trusts the headers hands you binary; a few sniff the magic bytes.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/undeclared'","edge_transcoding":"Nothing to transcode: everyone gets the gzip bytes labelled as text.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.truncated","family":"compress","flavor":"truncated","layer":"content-coding","url":"https://badhttp.dev/compress/truncated","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"A valid gzip stream cut at about 60% (x-badhttp-wire-bytes-of says exactly) — inside the DEFLATE data, before the CRC-32 and length trailer. Content-Length matches the bytes actually sent, so the transport looks complete; only the decoder can tell. Does your client report the error, or return the partial plaintext as if it were everything?","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/truncated'","edge_transcoding":"Inflates what it can and ends the body cleanly: a 200 with the partial plaintext (1,860 of 4,096 bytes at the default length; a correct prefix of 628,228 bytes at 1 MiB) and no error anywhere. For a document of a few bytes the partial text may be all of it.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.corrupt","family":"compress","flavor":"corrupt","layer":"content-coding","url":"https://badhttp.dev/compress/corrupt","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"A gzip stream with up to four bytes flipped in the middle of the DEFLATE data (x-badhttp-flipped-bytes says how many and where; fewer only for streams under 25 bytes). A decoder either rejects the stream outright, or inflates it — often to the full length, correct up to the damage and garbage after — and then fails the CRC; a decoder that does not check the CRC (see bad-crc) returns that garbage with no error at all. Either way it may have handed you some of the garbage first.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/corrupt'","edge_transcoding":"At the default length, a 200 with an EMPTY body: the decode fails, nothing is sent, no error anywhere. At 1 MiB it streamed the correct plaintext up to the damage and garbage after it, 60 bytes short, still no error — the CRC-skipping outcome.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.bad-crc","family":"compress","flavor":"bad-crc","layer":"content-coding","url":"https://badhttp.dev/compress/bad-crc","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"A gzip stream whose DEFLATE data is intact but whose CRC-32 trailer is wrong (the ISIZE is right). Every byte of plaintext is recoverable; the question is whether your client checks the checksum, and what it does with the data it already delivered when the check fails at the very end.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/bad-crc'","edge_transcoding":"Does not check the trailer: the full plaintext, no error.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.trailing-garbage","family":"compress","flavor":"trailing-garbage","layer":"content-coding","url":"https://badhttp.dev/compress/trailing-garbage","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"A complete, valid gzip member followed by 49 bytes of junk, all counted in Content-Length. RFC 1952 says a stream is a series of members; the junk is not one. Some decoders ignore it, some fail after having produced the whole plaintext, some warn.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/trailing-garbage'","edge_transcoding":"The full plaintext; the junk is dropped without a word.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.multi-member","family":"compress","flavor":"multi-member","layer":"content-coding","url":"https://badhttp.dev/compress/multi-member","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Two gzip members back to back, each holding half the document — valid per RFC 1952 §2.2, and what some servers and concatenating CDNs really emit. Some HTTP clients decode only the first member and return half the document with no error (httpx and Ruby's Net::HTTP among them, 2026-09-01).","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/multi-member'","edge_transcoding":"The first member only — half the document (2,048 of 4,096 bytes at the default length), no error.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.double","family":"compress","flavor":"double","layer":"content-coding","url":"https://badhttp.dev/compress/double","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Content-Encoding: gzip, gzip — the document gzipped twice, with both codings declared in order, exactly as RFC 9110 §8.4 allows. Clients that apply only the first coding hand you a gzip stream and call it text; clients that compare the header to \"gzip\" exactly do not decode at all.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/double'","edge_transcoding":"Removes exactly one layer and relabels the rest Content-Encoding: gzip — self-consistent (the header still matches the body), though RFC 9110 §7.7 forbids the transformation under no-transform, and the one case where the edge itself hands a client a coding it recognizes that the client never listed.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.double-hidden","family":"compress","flavor":"double-hidden","layer":"content-coding","url":"https://badhttp.dev/compress/double-hidden","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"The document gzipped twice but Content-Encoding says gzip once. Decoding \"succeeds\" and yields 1f 8b…: a gzip stream where your text should be. The bug proxies that compress already-compressed responses produce.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/double-hidden'","edge_transcoding":"Removes one layer and the header: gzip bytes labelled as text.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.deflate-raw","family":"compress","flavor":"deflate-raw","layer":"content-coding","url":"https://badhttp.dev/compress/deflate-raw","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Content-Encoding: deflate with a raw RFC 1951 stream — no zlib header, no Adler-32 — the historic mistake that made \"deflate\" untrustworthy. Some clients detect the missing wrapper and recover; some fail on byte one; some do not decode deflate at all.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/deflate-raw'","edge_transcoding":"Cannot inflate it: the raw bytes go through with the header removed. Only a client whose reported set lists gzip gets them with the header intact; listing deflate does not help — the header is removed for that client too.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.unknown-coding","family":"compress","flavor":"unknown-coding","layer":"content-coding","url":"https://badhttp.dev/compress/unknown-coding","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Content-Encoding: badhttp, a coding that does not exist, on a plain-text body. There is no rule for a client here: pass the bytes through, or refuse. Which does yours do, and does it tell you?","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/unknown-coding'","edge_transcoding":"Unrecognized, so untouched: everyone gets it as sent.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.uppercase","family":"compress","flavor":"uppercase","layer":"content-coding","url":"https://badhttp.dev/compress/uppercase","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Content-Encoding: GZIP. Coding names are case-insensitive (RFC 9110 §8.4.1); the body is ordinary gzip. Clients that compare the header case-sensitively leave it compressed.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/uppercase'","edge_transcoding":"Recognized: decoded to identity for a client without gzip.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.x-gzip","family":"compress","flavor":"x-gzip","layer":"content-coding","url":"https://badhttp.dev/compress/x-gzip","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Content-Encoding: x-gzip, the legacy alias that RFC 9110 §8.4.1.3 says a recipient SHOULD treat as gzip. The body is ordinary gzip.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/x-gzip'","edge_transcoding":"Not recognized as gzip: passed to everyone as sent, header and all, whatever they advertised.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.empty","family":"compress","flavor":"empty","layer":"content-coding","url":"https://badhttp.dev/compress/empty","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"Content-Encoding: gzip on a zero-byte body (Content-Length: 0) — what a proxy that strips a body but not its headers produces; with ?code=502 it is the stripped error page. An empty string is not a gzip stream; some decoders say so, some return nothing quietly. ?length= is accepted only at 0.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/empty'","edge_transcoding":"Header removed, Content-Length: 0.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.wrong-length","family":"compress","flavor":"wrong-length","layer":"content-coding","url":"https://badhttp.dev/compress/wrong-length","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"A correct gzip body whose Content-Length is the plaintext's length, not the wire's — the header a proxy leaves behind when it compresses without recounting. The declared bytes never arrive: the connection closes early (HTTP/1.1) or the stream is reset (HTTP/2), after the decoder has already seen a complete gzip stream. Which error do you get, if any? ?length= is at least 128 here, so the gzip member is really shorter than the length it claims.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/wrong-length'","edge_transcoding":"Decodes and re-chunks: the lie disappears and a client without gzip gets a complete, correct response.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.gzip-file","family":"compress","flavor":"gzip-file","layer":"content-coding","url":"https://badhttp.dev/compress/gzip-file","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"The .gz download trap: a file that already IS gzip, served as a download (Content-Type: application/gzip, Content-Disposition: attachment; filename=\"badhttp-compress.txt.gz\") — and, wrongly, with Content-Encoding: gzip as well, the way misconfigured static servers and object stores label .tar.gz files. A client that applies transport decoding saves the plaintext under a .gz name: silent corruption of the download. x-badhttp-wire-sha256 is the hash of the bytes that should land in the file; x-badhttp-plain-sha256 the hash of what lands if the client decoded.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/gzip-file'","edge_transcoding":"Commits the corruption itself: a client without gzip receives the plaintext, still labelled application/gzip with the .gz filename, Content-Encoding and Content-Length removed (observed 2026-09-02).","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"compress.bomb","family":"compress","flavor":"bomb","layer":"content-coding","url":"https://badhttp.dev/compress/bomb","method":"GET","request_headers":{"accept-encoding":"gzip"},"defect":"A decompression bomb, declared: one gzip member that inflates to ?size= MiB (default 8, max 32) from about 3 KB per MiB on the wire (x-badhttp-wire-bytes says exactly) — the same 64-byte line repeated. Only the requester's own memory is at risk: a client that decodes without a size limit allocates the whole thing. x-badhttp-plain-bytes states the inflated size, x-badhttp-plain-line carries the line without its newline (63 characters; the plaintext is that line plus a newline, repeated), and x-badhttp-plain-sha256 is the hash of the whole.","specs":["RFC 9110 §8.4","RFC 1950","RFC 1951","RFC 1952","RFC 7932","RFC 8878"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":true,"varies_by":["accept-encoding"],"curl":"curl --http1.1 --raw -sS -D - -H 'accept-encoding: gzip' --output - 'https://badhttp.dev/compress/bomb'","edge_transcoding":"Not served: a client whose reported set lacks gzip gets a 406 asking for Accept-Encoding: gzip. That set reads as empty for a client that sent no Accept-Encoding at all, or only \"*\" — requests RFC 9110 §12.5.3 says accept gzip — so the 406 is not negotiation but a refusal: the edge would otherwise inflate the member itself and ship the whole ?size= MiB as text (observed before the 406 was added), testing no decoder.","pinned_digest_header":"x-badhttp-plain-sha256","pinned_digest_note":"The response carries the SHA-256 and byte length of the PLAINTEXT it should decode to (x-badhttp-plain-sha256 / x-badhttp-plain-bytes). Pin those, not the compressed bytes: the wire size belongs to this runtime's zlib and is not a stable fact about the flavor.","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/pay","family":"402","flavor":"pay","layer":"payment","url":"https://badhttp.dev/402/pay","method":"GET","request_headers":{},"defect":"Returns 402 with x402 requirements in both generations at once: v2 in the PAYMENT-REQUIRED header, v1 in the JSON body (Base Sepolia unless you ask for mainnet with /402/pay/base or ?network=base). Send a valid PAYMENT-SIGNATURE (v2) or X-PAYMENT (v1) and the payment is verified and settled through a facilitator; you get a 200 with the transaction hash and a receipt in PAYMENT-RESPONSE (v2) or X-PAYMENT-RESPONSE (v1).","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/pay'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/never","family":"402","flavor":"never","layer":"payment","url":"https://badhttp.dev/402/never","method":"GET","request_headers":{},"defect":"Always 402, with perfectly valid requirements. Any PAYMENT-SIGNATURE (v2) or X-PAYMENT (v1) you send is ignored. Nothing is verified or settled. Does your client stop after one retry, or loop and re-sign forever?","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/never'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/reject","family":"402","flavor":"reject","layer":"payment","url":"https://badhttp.dev/402/reject","method":"GET","request_headers":{},"defect":"Valid 402; then every payment is rejected with a 402 carrying an error (default insufficient_funds; pick another with ?reason=). Nothing is settled. A client should surface the reason and stop, not re-sign.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/reject'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/slow","family":"402","flavor":"slow","layer":"payment","url":"https://badhttp.dev/402/slow","method":"GET","request_headers":{},"defect":"Valid 402; after you pay, the server sits on the request for ?seconds= (default 8, max 10) and then answers 504 with no receipt. In the real world you would not know whether you were charged. Here, nothing was.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/slow'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/crash","family":"402","flavor":"crash","layer":"payment","url":"https://badhttp.dev/402/crash","method":"GET","request_headers":{},"defect":"Valid 402; after you pay, a 500 with no PAYMENT-RESPONSE. A real server might have settled before it crashed. This one never does. Does your client treat this as \"paid\" or \"unpaid\"?","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/crash'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/bad-receipt","family":"402","flavor":"bad-receipt","layer":"payment","url":"https://badhttp.dev/402/bad-receipt","method":"GET","request_headers":{},"defect":"Valid 402; after you pay, a 200 whose receipt headers — PAYMENT-RESPONSE (v2) and X-PAYMENT-RESPONSE (v1) — are both garbage, not valid base64 JSON. Nothing was settled. Does your client still hand you the body, or throw it away because the receipt is bad?","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/bad-receipt'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/overpriced","family":"402","flavor":"overpriced","layer":"payment","url":"https://badhttp.dev/402/overpriced","method":"GET","request_headers":{},"defect":"A valid 402 that asks for 1,000,000 USDC. A client with a spending limit should refuse to sign. If yours signs anyway, the response says so; the authorization is discarded and never settled. A less friendly server would have taken it.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/overpriced'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/wrong-network","family":"402","flavor":"wrong-network","layer":"payment","url":"https://badhttp.dev/402/wrong-network","method":"GET","request_headers":{},"defect":"A valid-looking 402 whose only option is on eip155:424242, a chain nobody runs. A client should report \"no supported network\" and not sign. Nothing can be settled here by anyone.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/wrong-network'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/broken","family":"402","flavor":"broken","layer":"payment","url":"https://badhttp.dev/402/broken","method":"GET","request_headers":{},"defect":"GET /402/broken lists the flavors: not-base64, not-json, no-accepts, empty-accepts, no-extra, no-resource, version-99, decimal-amount, missing-header, v1-body. Each is a 402 that a sloppy client will mis-parse.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/not-base64","family":"402","flavor":"not-base64","layer":"payment","url":"https://badhttp.dev/402/broken/not-base64","method":"GET","request_headers":{},"defect":"PAYMENT-REQUIRED is not base64.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/not-base64'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/not-json","family":"402","flavor":"not-json","layer":"payment","url":"https://badhttp.dev/402/broken/not-json","method":"GET","request_headers":{},"defect":"PAYMENT-REQUIRED is base64 of something that is not JSON.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/not-json'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/no-accepts","family":"402","flavor":"no-accepts","layer":"payment","url":"https://badhttp.dev/402/broken/no-accepts","method":"GET","request_headers":{},"defect":"A PaymentRequired object with no \"accepts\" field at all.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/no-accepts'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/empty-accepts","family":"402","flavor":"empty-accepts","layer":"payment","url":"https://badhttp.dev/402/broken/empty-accepts","method":"GET","request_headers":{},"defect":"\"accepts\" is an empty list: nothing to pay with.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/empty-accepts'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/no-extra","family":"402","flavor":"no-extra","layer":"payment","url":"https://badhttp.dev/402/broken/no-extra","method":"GET","request_headers":{},"defect":"The requirement has no \"extra\" (no EIP-712 domain name/version), so the authorization cannot be signed correctly.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/no-extra'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/no-resource","family":"402","flavor":"no-resource","layer":"payment","url":"https://badhttp.dev/402/broken/no-resource","method":"GET","request_headers":{},"defect":"No \"resource\" object. The reference client treats it as optional and signs anyway; stricter clients refuse.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/no-resource'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/version-99","family":"402","flavor":"version-99","layer":"payment","url":"https://badhttp.dev/402/broken/version-99","method":"GET","request_headers":{},"defect":"x402Version is 99.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/version-99'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/decimal-amount","family":"402","flavor":"decimal-amount","layer":"payment","url":"https://badhttp.dev/402/broken/decimal-amount","method":"GET","request_headers":{},"defect":"\"amount\" is \"0.01\" instead of atomic units (\"10000\"). A client that does not validate will sign for 0.01 atomic units, i.e. nothing.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/decimal-amount'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/missing-header","family":"402","flavor":"missing-header","layer":"payment","url":"https://badhttp.dev/402/broken/missing-header","method":"GET","request_headers":{},"defect":"A 402 with v2-shaped requirements only in the JSON body (x402Version 2) and no PAYMENT-REQUIRED header. Genuinely broken: v2 keeps requirements in the header, and the reference client reads a body only when it says x402Version 1, so it finds nothing here.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/missing-header'","license":"CC0-1.0","badhttp_version":"0.13.0"}
{"id":"402/v1-body","family":"402","flavor":"v1-body","layer":"payment","url":"https://badhttp.dev/402/broken/v1-body","method":"GET","request_headers":{},"defect":"A spec-valid x402 v1 response: requirements only in the JSON body (maxAmountRequired, plain-string network name), no PAYMENT-REQUIRED header. Not malformed — /402/pay serves this same v1 body underneath its v2 header — but a strictly header-only v2 client sees no requirements, which is the trap.","specs":["x402 v2","x402 v1","RFC 9110 §15.5.3"],"conforms_rfc9112_syntax":true,"rfc9112_completeness_violation":false,"deterministic_bytes":false,"varies_by":["payment-signature","x-payment","date"],"curl":"curl --http1.1 --raw -sS -D - --output - 'https://badhttp.dev/402/broken/v1-body'","license":"CC0-1.0","badhttp_version":"0.13.0"}
