feat(hxprobe): httpx-based HTTP probe — full standalone package

Third Python implementation in the latency-tool progression.
Mirrors latprobe's feature set but replaces raw sockets with httpx,
gaining HTTP/2 support and redirect following.

Package layout (hxprobe/hxprobe/):
- probe.py: asyncio + httpx.AsyncClient with a _TimingStream transport
  wrapper that captures DNS/connect/TLS/TTFB/transfer phase timings via
  httpx event hooks (get_connection_stats, request_started, etc.).
  VerboseDetail captures resolved IP, TLS version/cipher/bits, cert CN/
  expiry/issuer (from httpx's SSLObject), and response headers.
  Options: timeout, verbose, follow_redirects, http2
- aggregate.py: summarize() → per-phase min/avg/max (same schema as latprobe)
- cli.py: run(args,stdout,stderr)->int; argparse with redirect_stdout/
  redirect_stderr + SystemExit catch; -n/--count, -c/--concurrency,
  --timeout, --fail, --json, -v/--verbose, --no-follow-redirects,
  --no-http2, -f/--file (URL list from file, mutually exclusive with args);
  multi-URL summary footer (tally + exit label) when len(urls) > 1;
  worst-exit-code logic mirrors Go/latprobe; JSON output is bare array
- duration.py: same parse_duration() as latprobe
- Exit codes: 0 ok, 1 usage, 2 dns, 3 connect, 4 timeout, 5 tls, 6 http≥400

Tests (hxprobe/tests/):
- test_probe.py: 18 hermetic tests using anyio + in-process ASGI servers
- test_cli.py: 36 hermetic tests (success, failures, JSON, verbose, -f flag,
  run-summary footer, worst-code accumulation)
- test_integration.py: pytest-marked @integration (excluded from hx-test)

Toolchain: uv + ruff + pytest; pyproject.toml with [dependency-groups];
hxprobe/Makefile standalone (help, deps, run, lint, fmt, test,
test-integration, check, clean); .python-version pins 3.14
Configs: 8 fixture files mirroring python/configs/ (all-ok through mixed)
USAGE.md: 16 runnable examples with real captured output

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 13:22:31 +02:00
parent 24ea9c9e71
commit 404b372070
24 changed files with 2924 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# Large mixed run — 30 URLs, 20 expected to succeed and 10 to fail, covering
# all five failure classes (dns/connect/timeout/tls/http) at once. A bigger
# sibling of mixed.txt, and a good demo of the multi-URL summary footer
# (only shown when more than one URL is probed) — the footer tallies exactly
# how many of the 10 fall into each class, since the single worst exit code
# can't show that on its own.
#
# --timeout 2s shortens the two timeout entries from the 10s default; --fail
# is required for the two 404s to count as failures (without it they're
# "successful" 404 responses, per Go http.DefaultClient semantics, and only
# 8 of the 30 would fail).
#
# Requires internet access.
#
# Run: uv run python -m hxprobe --fail --timeout 2s -f configs/large-mixed.txt
# Expected exit code: 6 (http wins — the highest class present)
# Expected: at least 10 of 30 fail, spanning all 5 classes. The *exact*
# per-class mix can shift with network conditions — same caveat as
# tls-errors.txt (badssl.com may reset/timeout instead of a clean TLS error)
# and timeout.txt (a sandboxed network may refuse instantly instead of
# timing out); a couple of the "OK" sites were swapped out during testing
# for being unreliable in some sandboxes (Wikipedia's bot detection,
# httpbin.org's frequent overload). Verified by an actual run:
# 30 URLs — 20 ok, 10 failed (dns: 2, connect: 3, timeout: 1, tls: 2,
# http: 2) → exit 6.
# ── OK (20) ──────────────────────────────────────────────────────────────
https://example.com
https://example.org
https://example.net
https://www.google.com
https://www.iana.org
https://github.com
https://www.debian.org
https://www.postgresql.org
https://www.mozilla.org
https://developer.mozilla.org
https://www.python.org
https://pypi.org
https://www.cloudflare.com
https://www.apache.org
https://www.rust-lang.org
https://go.dev
https://nodejs.org
https://www.w3.org
https://www.ietf.org
https://stackoverflow.com
# ── DNS failure (2) — .invalid is IANA-reserved, RFC 2606 ──────────────────
https://this-host-does-not-exist.invalid
http://no.such.host.invalid
# ── Connection refused (2) — loopback ports with no listener ───────────────
http://127.0.0.1:9999
http://127.0.0.1:19999
# ── Timeout (2) — RFC 5737 TEST-NET-1, non-routable ─────────────────────────
http://10.255.255.1/
http://192.0.2.1/
# ── TLS certificate errors (2) — badssl.com ─────────────────────────────────
https://expired.badssl.com/
https://self-signed.badssl.com/
# ── HTTP errors (2) — real 404s, need --fail to count as failures ──────────
https://www.google.com/this-page-does-not-exist-at-all-1234567890
https://github.com/this-repo-does-not-exist-abcxyz123/no-way