Plans (docs/plans/): - 2026-07-01-23-47-py-hxprobe-httpx.md — initial httpx probe design - 2026-07-02-09-32 through 14-05 — standalone project, toolchain, usage doc + Makefile, file input (-f), simplification pass, run-summary footer Summaries (docs/summaries/): one per completed feature, recording what was actually built, deviations from the plan, and verification steps Explanations (docs/explanations/): two deep-dives written during review — hxprobe concurrency model and worst-exit-code + render-loop analysis Usage (docs/usage/hxprobe.md): overview with pointer to hxprobe/USAGE.md for the full runnable reference Walkthrough (docs/py-latprobe-walkthrough.md): narrative tour of the latprobe Python package for interview / code-review context CHANGELOG.md: entries for all hxprobe features (toolchain, usage doc, file input, simplification, run-summary footer) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.4 KiB
Plan: read target URLs from a file for hxprobe
Context
hxprobe currently only accepts URLs as positional CLI arguments
(hxprobe/hxprobe/cli.py:368, nargs="+"). The user wants a file-based
input mode too, matching the existing convention simple.py/phases.py
already use (python/simple.py:20-30's load_sites(): plain text, one URL
per line, #-comments and blank lines skipped, first whitespace-separated
token taken per line).
User-confirmed decisions:
- Mutually exclusive with positional URL args (either pass URLs on the
command line, or
-f FILE, never both). - hxprobe only —
latprobeis intentionally left untouched. - Add example fixture files (
hxprobe/configs/*.txt, mirroringpython/configs/*.txt's exact set: all-ok, dns-failure, connection-refused, timeout, tls-errors, http-errors, mixed) plus one new section in the existinghxprobe/USAGE.mddemonstrating the flag.
Implementation
hxprobe/hxprobe/cli.py:
urlspositional becomesnargs="*"(wasnargs="+") — no longer required on its own, since-fis now a second valid source.- New flag:
-f, --file PATH— "read URLs from a file, one per line,#comments allowed (mutually exclusive with positional url args)". Placed right after theurlspositional definition in the argparse block, since the two are the two ways of specifying what to probe. - New helper
_load_urls(path: str) -> list[str], duplicating (not importing)simple.py'sload_sites()logic — consistent with hxprobe's established "imports nothing outside its own directory" rule from the standalone-extraction work. - After
parser.parse_args(), manual validation (mirrors the existing--timeoutinvalid-value handling style — write to the injectedstderr,return EXIT_USAGE, rather than routing throughargparse's mutually-exclusive-group machinery, which doesn't mix cleanly with a variadic positional):- both
ns.urlsandns.filegiven →parser.error(...)(usage error, consistent with how_Parser.error()already handles bad usage) - neither given →
parser.error(...) ns.filegiven but unreadable (FileNotFoundError/OSError) →stderr.write(...);return EXIT_USAGEns.filegiven but yields zero URLs → same treatment- otherwise
urls = _load_urls(ns.file)orurls = ns.urls
- both
hxprobe/tests/test_cli.py: new hermetic tests — successful multi-URL
run from a file, missing-file error, empty-file error, and the
both-sources-given usage error. Uses a temp file (tempfile), no network
needed for the parsing-error cases.
hxprobe/configs/*.txt (new directory) — same 7 fixtures as
python/configs/, adapted:
all-ok.txt,dns-failure.txt,connection-refused.txt,tls-errors.txt— same URLs, same behavior (DNS/TCP/TLS failures are identical regardless of HTTP client sophistication); only the header comments change (hxprobe -f configs/<name>.txtinstead ofpython3.14 python/simple.py ...).timeout.txt— same two targets (10.255.255.1,192.0.2.1, RFC 5737 TEST-NET-1) as the original; "expected exit code" documents normal-network behavior (exit 4), same caveat the original file already carries about network-dependent behavior.http-errors.txt— same 404 URLs; header comment updated to show the demo command with--fail(hxprobe treats 4xx as success without--fail, unlikesimple.py, which always raises on HTTPError) — "expected exit code" becomes 6, notsimple.py's blanket 1.mixed.txt— same mixed set; demo command includes--fail; expected exit code recalculated as the worst code across the included classes (dns=2, connect=3, tls=5, http=6 with--fail) → 6.- Each header's "Expected exit code" will be verified by actually running
the fixture through
hxprobe -f ...during implementation, not assumed from thesimple.pyoriginals — hxprobe's worst-code-wins exit scheme (hxprobe/hxprobe/cli.py:455-481) differs fundamentally fromsimple.py's blanket 0/1.
hxprobe/USAGE.md: new section "Reading URLs from a file (-f)",
placed after the "Multiple URLs" case (same family of "what to probe"
examples) — command + real captured output using configs/all-ok.txt or
configs/mixed.txt, plus a short list of the other fixture files available
and what each demonstrates.
Docs housekeeping (per CLAUDE.md convention): save this plan under
docs/plans/, write a summary under docs/summaries/ after implementation,
append a CHANGELOG.md entry.
Verification
cd hxprobe && uv run python -m hxprobe -f configs/all-ok.txt— probes all 3 URLs, exit 0.cd hxprobe && uv run python -m hxprobe -f configs/dns-failure.txt— exit 2;connection-refused.txt→ exit 3;tls-errors.txt→ exit 5;--fail -f configs/http-errors.txt→ exit 6;--fail -f configs/mixed.txt→ exit 6 (confirms the worst-code documented in each header is accurate).uv run python -m hxprobe https://example.com -f configs/all-ok.txt— usage error (both sources given).uv run python -m hxprobe -f /no/such/file— usage error, clear message.cd hxprobe && make check— new hermetic tests pass alongside the existing 28.- Re-run the new
hxprobe/USAGE.mdsection's command and confirm captured output matches what's printed in the doc. grep -n "import" hxprobe/hxprobe/cli.py— confirm no new import frompython/simple.pyor anywhere outsidehxprobe/.