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:
7
hxprobe/configs/all-ok.txt
Normal file
7
hxprobe/configs/all-ok.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# All sites are expected to respond successfully.
|
||||
# Run: uv run python -m hxprobe -f configs/all-ok.txt
|
||||
# Expected exit code: 0
|
||||
|
||||
https://example.com
|
||||
https://www.google.com
|
||||
https://www.iana.org
|
||||
7
hxprobe/configs/connection-refused.txt
Normal file
7
hxprobe/configs/connection-refused.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Connection-refused errors — loopback addresses with no server on those ports.
|
||||
# The OS rejects the TCP SYN immediately, so these fail in milliseconds.
|
||||
# Run: uv run python -m hxprobe -f configs/connection-refused.txt
|
||||
# Expected exit code: 3
|
||||
|
||||
http://127.0.0.1:9999
|
||||
http://127.0.0.1:19999
|
||||
9
hxprobe/configs/dns-failure.txt
Normal file
9
hxprobe/configs/dns-failure.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# DNS resolution failures — hostnames that cannot be resolved.
|
||||
# .invalid is an IANA-reserved TLD guaranteed never to resolve (RFC 2606).
|
||||
# Run: uv run python -m hxprobe -f configs/dns-failure.txt
|
||||
# Expected exit code: 2
|
||||
|
||||
https://this-host-does-not-exist.invalid
|
||||
http://no.such.host.invalid
|
||||
|
||||
# Also exercises the blank-line and comment-line parser paths.
|
||||
22
hxprobe/configs/http-errors.txt
Normal file
22
hxprobe/configs/http-errors.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
# HTTP error status codes — without --fail, hxprobe treats 4xx/5xx as a
|
||||
# normal (successful) probe outcome, matching Go's http.DefaultClient
|
||||
# semantics. --fail opts back into "HTTP error = failure", which is what
|
||||
# this fixture demonstrates. The URLs below are real paths that reliably
|
||||
# return 404 on stable public servers.
|
||||
#
|
||||
# Note: a genuine 500 from a well-known server is hard to provoke on demand.
|
||||
# These 404s are sufficient to demonstrate --fail's effect on the exit code.
|
||||
#
|
||||
# Requires internet access.
|
||||
#
|
||||
# Run: uv run python -m hxprobe --fail -f configs/http-errors.txt
|
||||
# Expected exit code: 6 (0 without --fail — the requests still succeed)
|
||||
|
||||
# 404 from Google
|
||||
https://www.google.com/this-page-does-not-exist-at-all-1234567890
|
||||
|
||||
# 404 from GitHub
|
||||
https://github.com/this-repo-does-not-exist-abcxyz123/no-way
|
||||
|
||||
# 404 from IANA
|
||||
https://www.iana.org/this-page-does-not-exist-either
|
||||
67
hxprobe/configs/large-mixed.txt
Normal file
67
hxprobe/configs/large-mixed.txt
Normal 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
|
||||
23
hxprobe/configs/mixed.txt
Normal file
23
hxprobe/configs/mixed.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
# Mixed — one entry from each error class alongside a successful site.
|
||||
# Shows that OK and FAIL lines can interleave in the same run, and that the
|
||||
# exit code is the *worst* code across every URL (2=dns, 3=connect, 5=tls,
|
||||
# 6=http-with-fail — 6 wins here). Timeout is omitted so the run completes
|
||||
# in a few seconds; --fail is needed for the 404 line to count as a failure.
|
||||
#
|
||||
# Run: uv run python -m hxprobe --fail -f configs/mixed.txt
|
||||
# Expected exit code: 6
|
||||
|
||||
# Success
|
||||
https://example.com
|
||||
|
||||
# DNS failure
|
||||
https://no.such.host.invalid
|
||||
|
||||
# Connection refused (loopback, no server)
|
||||
http://127.0.0.1:9999
|
||||
|
||||
# TLS certificate error
|
||||
https://self-signed.badssl.com/
|
||||
|
||||
# HTTP error (server responded with 404; needs --fail to count as a failure)
|
||||
https://github.com/this-repo-does-not-exist-abcxyz123/no-way
|
||||
16
hxprobe/configs/timeout.txt
Normal file
16
hxprobe/configs/timeout.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
# Timeout — non-routable IP addresses that accept no TCP traffic.
|
||||
# The kernel sends a SYN but never gets a reply; hxprobe waits the full
|
||||
# --timeout per host before giving up. Default --timeout is 10s (~20s total
|
||||
# for two hosts); --timeout 2s below shortens the demo.
|
||||
#
|
||||
# 10.255.255.1 and 192.0.2.1 (RFC 5737 TEST-NET-1, documentation-only range)
|
||||
# are guaranteed to be unreachable on any normal network. (In some sandboxed
|
||||
# dev environments one of these may instead get an immediate "connection
|
||||
# refused" from the network layer rather than a true timeout — if that
|
||||
# happens here, the other target still demonstrates the timeout path.)
|
||||
#
|
||||
# Run: uv run python -m hxprobe --timeout 2s -f configs/timeout.txt
|
||||
# Expected exit code: 4
|
||||
|
||||
http://10.255.255.1/
|
||||
http://192.0.2.1/
|
||||
22
hxprobe/configs/tls-errors.txt
Normal file
22
hxprobe/configs/tls-errors.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
# TLS certificate errors — badssl.com provides endpoints with intentionally
|
||||
# broken certificates. hxprobe uses the same stdlib ssl verification as
|
||||
# latprobe and rejects them by default.
|
||||
#
|
||||
# Note: badssl.com may intermittently reset the connection instead of
|
||||
# completing the TLS handshake. The error message will then read
|
||||
# "[Errno 54] Connection reset by peer" rather than CERTIFICATE_VERIFY_FAILED,
|
||||
# but hxprobe still correctly reports a tls/connect failure in either case.
|
||||
#
|
||||
# Requires internet access.
|
||||
#
|
||||
# Run: uv run python -m hxprobe -f configs/tls-errors.txt
|
||||
# Expected exit code: 5
|
||||
|
||||
# Certificate has expired
|
||||
https://expired.badssl.com/
|
||||
|
||||
# Certificate is self-signed (not trusted by the system CA store)
|
||||
https://self-signed.badssl.com/
|
||||
|
||||
# Certificate chain is incomplete (intermediate CA missing)
|
||||
https://incomplete-chain.badssl.com/
|
||||
Reference in New Issue
Block a user