Zero-dependency Python 3.11+ single file. 16 failure modes selectable by
URL path and combinable with '+': no-range, lies-about-accept-ranges,
etag-changes, flaky-reset (TCP RST mid-body, clean on the 3rd try),
slow-loris, redirect-chain, 401-basic, 401-digest, 403-without-referer,
416-always, content-length-mismatch, expiring-signed-url, throttled,
chunked-no-length, utf8/legacy content-disposition. Deterministic
synthetic bodies (byte i = f(seed, path, i)) with a /sha256/ reference
route so any range is independently verifiable. /__control {"reset":true}
clears flaky-mode counters between cases.
selftest.py exercises every mode (43 checks) and is registered as the
`testserver_selftest` CTest. README documents the full surface — CORE's
M1 DoD is written against it.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
3.9 KiB
tools/testserver — the hostile HTTP server
Every failure mode a real download hits, reproducible on demand. Standard-library Python
only (>= 3.11); no pip install. Owned by lane PKG/QA. CORE's M1 definition of done is
written in terms of this server.
tools/testserver/testserver.py --port 8080
tools/testserver/testserver.py --port 0 # ephemeral; the chosen port is printed to stdout
python3 tools/testserver/selftest.py # smoke-test every mode (also a CTest)
Request shape
GET /<mode>[+<mode>...]/file/<size>
GET /<mode>/sha256/<size> -> {"sha256": "<hex>", "size": <n>} reference digest
GET /<mode>/sign/<size>?ttl=<sec> -> {"url": "...", "exp": <unixtime>} (signed-URL mode)
<size> is a byte count with an optional K/M/G suffix (binary: KiB/MiB/GiB) —
1048576, 64K, 512M, 5G. The body is deterministic: byte i is a function of
(--seed, path, i), so any range is independently verifiable and the whole file has a
stable SHA-256 (fetch it from the /sha256/ route). Different paths/modes have different
content; a given path is byte-stable across requests except where a mode says otherwise
(etag-changes still serves stable bytes; only its ETag moves).
Combine modes with +: /throttled+etag-changes/file/1G.
Modes
| Mode | Behaviour |
|---|---|
(omitted) / plain |
Well-behaved: honours Range, stable ETag, correct Content-Length, Accept-Ranges: bytes. |
no-range |
No Accept-Ranges; Range ignored; always 200 full body. |
lies-about-accept-ranges |
Advertises Accept-Ranges: bytes but ignores Range and returns 200. |
etag-changes |
ETag differs on every response. A Range with a non-matching If-Range gets 200 full — i.e. "the file changed under you". |
flaky-reset |
Sends ~half the requested bytes then aborts the connection with a TCP RST. The 1st and 2nd attempt for a given path+range fail this way; the 3rd succeeds cleanly. POST /__control {"reset":true} clears the counters. |
slow-loris |
Status line, headers, and the first bytes are dribbled out one byte at a time for --loris-seconds, then the rest streams normally. Connection: close. |
redirect-chain |
302 --redirect-depth times (default 5) before the real resource. Query string is preserved across hops. |
401-basic |
HTTP Basic; credentials test / test. |
401-digest |
HTTP Digest, qop=auth; credentials test / test. |
403-without-referer |
403 unless Referer names this server's origin; otherwise serves normally. |
416-always |
Any Range request → 416 with Content-Range: bytes */<size>. A plain GET still returns 200 so the re-probe path is exercised. |
content-length-mismatch |
Content-Length header is correct-looking but the server sends ~half and closes unclean. |
expiring-signed-url |
Requires ?exp=&sig=. Past exp → 403 {"error":"expired"}; bad/missing sig → 403. Get a fresh URL from /…/sign/<size>?ttl=<sec>. Pair with download.refreshUrl. |
throttled |
Body rate-limited to --throttle-bps (default 1 MiB/s). |
chunked-no-length |
Transfer-Encoding: chunked, no Content-Length — size unknown until the stream ends. |
utf8-content-disposition |
Content-Disposition: attachment; filename="rates.pdf"; filename*=UTF-8''%E2%82%AC%20rates.pdf (RFC 5987 → "€ rates.pdf"). |
legacy-content-disposition |
Content-Disposition with an RFC 2047 MIME encoded-word filename — the classic mojibake source. |
Control & health
| Route | |
|---|---|
GET /__health |
200 ok |
POST /__control {"reset": true} |
Clears per-path attempt counters (flaky-reset). Call it between test cases. |
Flags
--host (default 127.0.0.1) · --port (0 = ephemeral) · --seed (content seed,
default 1) · --loris-seconds (default 5) · --redirect-depth (default 5) ·
--throttle-bps (default 1048576) · --verbose.