feat(py): step 1 refinement — error-path example configs for simple.py
- python/configs/: 7 purpose-built config files, one per error class: all-ok.txt, dns-failure.txt, connection-refused.txt, timeout.txt, tls-errors.txt (badssl.com), http-errors.txt (real 404 paths on stable hosts), mixed.txt (one of each, no timeout) - python/configs/usage.md: runnable sh commands + expected output per config, plus quick-sweep and full-sweep loops; commands written for running from the python/ directory - docs/custom-usage-examples.md: example 11 added (all failure classes at once) - docs/usage/py-simple.md: added pointer to python/configs/ and corrected limitation note (4xx/5xx are FAIL, not OK) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -284,6 +284,40 @@ JSON output lets you pipe results to `jq` for filtering.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Example 11 — 3 working sites, one of each error kind (exit code 6)
|
||||||
|
|
||||||
|
Minimal URL set that exercises every failure class simultaneously.
|
||||||
|
Three real sites succeed; five targets each trigger a distinct error.
|
||||||
|
`--fail` is required to surface the HTTP 4xx as an exit code.
|
||||||
|
**Expected exit code: 6** (highest code wins; HTTP error = 6 > TLS = 5 > timeout = 4 > refused = 3 > DNS = 2).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./go/latprobe -n 5 --fail --timeout 3s \
|
||||||
|
https://www.cloudflare.com \
|
||||||
|
https://www.github.com \
|
||||||
|
https://www.wikipedia.org \
|
||||||
|
https://nonexistent-host.invalid \
|
||||||
|
http://127.0.0.1:1 \
|
||||||
|
https://192.0.2.1 \
|
||||||
|
https://self-signed.badssl.com \
|
||||||
|
https://httpbin.org/status/404
|
||||||
|
```
|
||||||
|
|
||||||
|
| URL | Expected outcome | Exit-code contribution |
|
||||||
|
| --- | --------------- | ---------------------- |
|
||||||
|
| cloudflare.com | success | — |
|
||||||
|
| github.com | success | — |
|
||||||
|
| wikipedia.org | success | — |
|
||||||
|
| nonexistent-host.invalid | DNS failure | 2 |
|
||||||
|
| 127.0.0.1:1 | connection refused | 3 |
|
||||||
|
| 192.0.2.1 | timeout (3 s × 5 samples) | 4 |
|
||||||
|
| self-signed.badssl.com | TLS handshake failure | 5 |
|
||||||
|
| httpbin.org/status/404 | HTTP 404 (with --fail) | 6 |
|
||||||
|
|
||||||
|
_Estimated runtime: ~18–22 s — 8 workers cover all URLs in parallel; the non-routable IP (192.0.2.1) drives the wall time at 3 s × 5 samples = 15 s._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Exit code reference
|
## Exit code reference
|
||||||
|
|
||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
|
|||||||
7
python/configs/all-ok.txt
Normal file
7
python/configs/all-ok.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# All sites are expected to respond successfully.
|
||||||
|
# Run: python3.14 python/simple.py python/configs/all-ok.txt
|
||||||
|
# Expected exit code: 0
|
||||||
|
|
||||||
|
https://example.com
|
||||||
|
https://www.google.com
|
||||||
|
https://www.iana.org
|
||||||
7
python/configs/connection-refused.txt
Normal file
7
python/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: python3.14 python/simple.py python/configs/connection-refused.txt
|
||||||
|
# Expected exit code: 1
|
||||||
|
|
||||||
|
http://127.0.0.1:9999
|
||||||
|
http://127.0.0.1:19999
|
||||||
9
python/configs/dns-failure.txt
Normal file
9
python/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: python3.14 python/simple.py python/configs/dns-failure.txt
|
||||||
|
# Expected exit code: 1
|
||||||
|
|
||||||
|
https://this-host-does-not-exist.invalid
|
||||||
|
http://no.such.host.invalid
|
||||||
|
|
||||||
|
# Also exercises the blank-line and comment-line parser paths.
|
||||||
21
python/configs/http-errors.txt
Normal file
21
python/configs/http-errors.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# HTTP error status codes — urllib.request.urlopen() raises HTTPError for
|
||||||
|
# 4xx/5xx responses, so simple.py reports these as FAIL even though the server
|
||||||
|
# responded. 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 that HTTP errors are treated as FAIL.
|
||||||
|
#
|
||||||
|
# Requires internet access.
|
||||||
|
#
|
||||||
|
# Run: python3.14 python/simple.py python/configs/http-errors.txt
|
||||||
|
# Expected exit code: 1
|
||||||
|
|
||||||
|
# 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
|
||||||
21
python/configs/mixed.txt
Normal file
21
python/configs/mixed.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Mixed — one entry from each error class alongside a successful site.
|
||||||
|
# Shows that OK and FAIL lines can interleave in the same run.
|
||||||
|
# Timeout is omitted here so the run completes in a few seconds.
|
||||||
|
#
|
||||||
|
# Run: python3.14 python/simple.py python/configs/mixed.txt
|
||||||
|
# Expected exit code: 1 (any FAIL drives exit to 1)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
https://github.com/this-repo-does-not-exist-abcxyz123/no-way
|
||||||
14
python/configs/timeout.txt
Normal file
14
python/configs/timeout.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Timeout — non-routable IP addresses that accept no TCP traffic.
|
||||||
|
# The kernel sends a SYN but never gets a reply; simple.py waits the full
|
||||||
|
# 10-second hard-coded timeout per host before printing FAIL.
|
||||||
|
#
|
||||||
|
# WARNING: this config takes ~20 seconds to complete (10 s per host).
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# Run: python3.14 python/simple.py python/configs/timeout.txt
|
||||||
|
# Expected exit code: 1
|
||||||
|
|
||||||
|
http://10.255.255.1/
|
||||||
|
http://192.0.2.1/
|
||||||
21
python/configs/tls-errors.txt
Normal file
21
python/configs/tls-errors.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# TLS certificate errors — badssl.com provides endpoints with intentionally
|
||||||
|
# broken certificates. Python's ssl module 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 simple.py still correctly reports FAIL in either case.
|
||||||
|
#
|
||||||
|
# Requires internet access.
|
||||||
|
#
|
||||||
|
# Run: python3.14 python/simple.py python/configs/tls-errors.txt
|
||||||
|
# Expected exit code: 1
|
||||||
|
|
||||||
|
# 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/
|
||||||
185
python/configs/usage.md
Normal file
185
python/configs/usage.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# `simple.py` — Example Config Files
|
||||||
|
|
||||||
|
Each `.txt` file in this directory targets a distinct error path so you can
|
||||||
|
observe `simple.py`'s behaviour for every failure class. Run them from the
|
||||||
|
`python/` directory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `all-ok.txt` — all sites respond successfully
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python simple.py configs/all-ok.txt
|
||||||
|
echo "exit: $?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
OK xxx.xx ms https://example.com
|
||||||
|
OK xxx.xx ms https://www.google.com
|
||||||
|
OK xxx.xx ms https://www.iana.org
|
||||||
|
exit: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `dns-failure.txt` — DNS resolution fails
|
||||||
|
|
||||||
|
Hostnames use the `.invalid` TLD (RFC 2606), which is guaranteed never to
|
||||||
|
resolve. Fails in milliseconds.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python simple.py configs/dns-failure.txt
|
||||||
|
echo "exit: $?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
FAIL ([Errno 8] nodename nor servname provided, or not known) https://this-host-does-not-exist.invalid
|
||||||
|
FAIL ([Errno 8] nodename nor servname provided, or not known) http://no.such.host.invalid
|
||||||
|
exit: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `connection-refused.txt` — TCP connection refused
|
||||||
|
|
||||||
|
Loopback addresses with no server listening. The OS rejects the SYN immediately
|
||||||
|
(sub-millisecond failure).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python simple.py configs/connection-refused.txt
|
||||||
|
echo "exit: $?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
FAIL ([Errno 61] Connection refused) http://127.0.0.1:9999
|
||||||
|
FAIL ([Errno 61] Connection refused) http://127.0.0.1:19999
|
||||||
|
exit: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `timeout.txt` — connect hangs until timeout
|
||||||
|
|
||||||
|
Non-routable IPs (private/documentation ranges) silently drop TCP SYNs.
|
||||||
|
`simple.py` waits the full **10-second** timeout per host.
|
||||||
|
|
||||||
|
> ⚠️ This run takes approximately **20 seconds** to complete.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python simple.py configs/timeout.txt
|
||||||
|
echo "exit: $?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output (after ~20 s):
|
||||||
|
```
|
||||||
|
FAIL (timed out) http://10.255.255.1/
|
||||||
|
FAIL (timed out) http://192.0.2.1/
|
||||||
|
exit: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `tls-errors.txt` — TLS certificate errors
|
||||||
|
|
||||||
|
Uses [badssl.com](https://badssl.com) endpoints with intentionally broken
|
||||||
|
certificates. Requires internet access; badssl.com must be reachable.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python simple.py configs/tls-errors.txt
|
||||||
|
echo "exit: $?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
FAIL ([SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired …) https://expired.badssl.com/
|
||||||
|
FAIL ([SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate …) https://self-signed.badssl.com/
|
||||||
|
FAIL ([SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer …) https://incomplete-chain.badssl.com/
|
||||||
|
exit: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** badssl.com occasionally resets connections mid-handshake. When that
|
||||||
|
> happens, the error reads `[Errno 54] Connection reset by peer` instead of
|
||||||
|
> `CERTIFICATE_VERIFY_FAILED`. The key behaviour is the same — every entry is
|
||||||
|
> reported as `FAIL` and the script exits 1. This demonstrates that `simple.py`
|
||||||
|
> handles both SSL-level and network-level TLS failures uniformly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `http-errors.txt` — HTTP 4xx / 5xx status codes
|
||||||
|
|
||||||
|
`urllib.request.urlopen()` raises `HTTPError` for 4xx/5xx, so `simple.py`
|
||||||
|
reports these as `FAIL` even though the server responded. The URLs are real
|
||||||
|
paths that reliably return 404 on stable public servers. Requires internet
|
||||||
|
access.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python simple.py configs/http-errors.txt
|
||||||
|
echo "exit: $?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
FAIL (HTTP Error 404: Not Found) https://www.google.com/this-page-does-not-exist-at-all-1234567890
|
||||||
|
FAIL (HTTP Error 404: Not Found) https://github.com/this-repo-does-not-exist-abcxyz123/no-way
|
||||||
|
FAIL (HTTP Error 404: Not Found) https://www.iana.org/this-page-does-not-exist-either
|
||||||
|
exit: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `mixed.txt` — one of each (no timeout)
|
||||||
|
|
||||||
|
One successful site followed by one entry from each fast error class (DNS,
|
||||||
|
connection-refused, TLS, HTTP). Timeout is excluded so the run completes
|
||||||
|
quickly.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python simple.py configs/mixed.txt
|
||||||
|
echo "exit: $?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
OK xxx.xx ms https://example.com
|
||||||
|
FAIL ([Errno 8] nodename nor servname …) https://no.such.host.invalid
|
||||||
|
FAIL ([Errno 61] Connection refused) http://127.0.0.1:9999
|
||||||
|
FAIL ([SSL: CERTIFICATE_VERIFY_FAILED] …) https://self-signed.badssl.com/
|
||||||
|
FAIL (HTTP Error 404: Not Found) https://github.com/this-repo-does-not-exist-abcxyz123/no-way
|
||||||
|
exit: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** the TLS entry may occasionally show `[Errno 54] Connection reset by peer`
|
||||||
|
> instead of `CERTIFICATE_VERIFY_FAILED` — badssl.com sometimes drops the connection
|
||||||
|
> before the handshake completes. Both are reported as `FAIL`; run `tls-errors.txt`
|
||||||
|
> in isolation for consistent cert-verification messages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Run all configs
|
||||||
|
|
||||||
|
Iterate over every config file (skip `timeout.txt` for a quick sweep, or omit
|
||||||
|
the exclusion to run all):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Quick sweep — skips the slow timeout config
|
||||||
|
for f in configs/*.txt; do
|
||||||
|
[[ "$f" == *timeout* ]] && continue
|
||||||
|
echo
|
||||||
|
echo "=== $f ==="
|
||||||
|
python simple.py "$f"
|
||||||
|
echo "exit: $?"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Full sweep — includes timeout (~20 s extra)
|
||||||
|
for f in configs/*.txt; do
|
||||||
|
echo
|
||||||
|
echo "=== $f ==="
|
||||||
|
python simple.py "$f"
|
||||||
|
echo "exit: $?"
|
||||||
|
done
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user