Files
samiandClaude Sonnet 5 3325efc1c8 pkg: add tools/testserver — the hostile HTTP server
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
2026-09-09 19:22:36 +04:00
..

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 exp403 {"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.