diff --git a/docs/usage/py-phases.md b/docs/usage/py-phases.md index 6891580..e3f5386 100644 --- a/docs/usage/py-phases.md +++ b/docs/usage/py-phases.md @@ -124,18 +124,10 @@ Multiple URLs are separated by a blank line, matching Go's text output style. ## Using the error-path example configs The `python/configs/` files from `simple.py` work identically with `phases.py`. -Compare the error handling between the two tools: - -```sh -# DNS failure -python phases.py configs/dns-failure.txt - -# Connection refused — note DNS phase is present, TCP is partial -python phases.py configs/connection-refused.txt - -# TLS errors — all three phases up to TLS are present -python phases.py configs/tls-errors.txt -``` +See [`python/configs/usage-phases.md`](../../python/configs/usage-phases.md) for +runnable shell commands and expected output for every config file, including a +side-by-side comparison of how `phases.py` and `simple.py` differ on HTTP 4xx +responses. ## Limitations diff --git a/docs/usage/py-simple.md b/docs/usage/py-simple.md index a380003..614784d 100644 --- a/docs/usage/py-simple.md +++ b/docs/usage/py-simple.md @@ -64,7 +64,7 @@ Exit code: `1` failure class (DNS, connection-refused, timeout, TLS-cert errors, HTTP 4xx/5xx, and a mixed scenario). Each file includes a comment explaining what it exercises. -See [`python/configs/usage.md`](../../python/configs/usage.md) for the runnable +See [`python/configs/usage-simple.md`](../../python/configs/usage-simple.md) for the runnable shell commands and expected output for each file. ## Limitations diff --git a/python/configs/usage-phases.md b/python/configs/usage-phases.md new file mode 100644 index 0000000..0124358 --- /dev/null +++ b/python/configs/usage-phases.md @@ -0,0 +1,347 @@ +# `phases.py` — Example Config Files + +Each `.txt` file in this directory targets a distinct error path so you can +observe `phases.py`'s per-phase behaviour for every failure class. Run them +from the `python/` directory. + +A key difference from `simple.py`: on failure, `phases.py` shows the phases +that *did* complete before the error — giving you partial timing up to the +point of failure. + +--- + +## Direct URL (no config file) + +`phases.py` also accepts bare URLs as positional arguments: + +```sh +python phases.py https://example.com +``` + +Expected output: +``` +https://example.com (200) + DNS lookup : 14.19 ms + TCP connect : 9.60 ms + TLS handshake : 16.44 ms + Server (TTFB) : 70.29 ms + Transfer : 0.13 ms + ───────────────────────────── + Total : 118.14 ms +``` + +For `http://` URLs the TLS row is omitted: + +```sh +python phases.py http://example.com +``` + +``` +http://example.com (200) + DNS lookup : 13.61 ms + TCP connect : 10.56 ms + Server (TTFB) : 14.51 ms + Transfer : 0.08 ms + ───────────────────────────── + Total : 38.91 ms +``` + +--- + +## `all-ok.txt` — all sites respond successfully + +```sh +python phases.py configs/all-ok.txt +echo "exit: $?" +``` + +Expected output (latencies vary; multiple results separated by a blank line): +``` +https://example.com (200) + DNS lookup : 3.82 ms + TCP connect : 10.04 ms + TLS handshake : 17.12 ms + Server (TTFB) : 28.59 ms + Transfer : 0.09 ms + ───────────────────────────── + Total : 66.16 ms + +https://www.google.com (200) + DNS lookup : 14.31 ms + TCP connect : 8.47 ms + TLS handshake : 24.61 ms + Server (TTFB) : 95.24 ms + Transfer : 33.96 ms + ───────────────────────────── + Total : 182.67 ms + +https://www.iana.org (200) + DNS lookup : 23.19 ms + TCP connect : 9.77 ms + TLS handshake : 14.51 ms + Server (TTFB) : 68.53 ms + Transfer : 0.20 ms + ───────────────────────────── + Total : 125.93 ms +exit: 0 +``` + +--- + +## `dns-failure.txt` — DNS resolution fails + +DNS fails immediately — no TCP or TLS phases run. Only `Total` is shown +alongside the error (DNS lookup itself is not shown since it never completed). + +```sh +python phases.py configs/dns-failure.txt +echo "exit: $?" +``` + +Expected output: +``` +https://this-host-does-not-exist.invalid (FAILED) + ───────────────────────────── + Total : 2.67 ms + ✗ dns: [Errno 8] nodename nor servname provided, or not known + +http://no.such.host.invalid (FAILED) + ───────────────────────────── + Total : 0.65 ms + ✗ dns: [Errno 8] nodename nor servname provided, or not known +exit: 1 +``` + +--- + +## `connection-refused.txt` — TCP connection refused + +DNS succeeds (loopback resolves instantly), TCP connect fails immediately. +Both DNS and TCP phases are shown as partial timing. + +```sh +python phases.py configs/connection-refused.txt +echo "exit: $?" +``` + +Expected output: +``` +http://127.0.0.1:9999 (FAILED) + DNS lookup : 1.03 ms + TCP connect : 0.82 ms + ───────────────────────────── + Total : 1.88 ms + ✗ connect: [Errno 61] Connection refused + +http://127.0.0.1:19999 (FAILED) + DNS lookup : 0.01 ms + TCP connect : 0.40 ms + ───────────────────────────── + Total : 0.41 ms + ✗ connect: [Errno 61] Connection refused +exit: 1 +``` + +--- + +## `timeout.txt` — connect hangs until timeout + +DNS resolves, TCP SYN is sent but never gets a reply. `phases.py` waits the +full **10-second** timeout per host before printing FAIL. + +> ⚠️ This run takes approximately **20 seconds** to complete. + +```sh +python phases.py configs/timeout.txt +echo "exit: $?" +``` + +Expected output (after ~20 s): +``` +http://10.255.255.1/ (FAILED) + DNS lookup : x.xx ms + TCP connect : 10000.xx ms + ───────────────────────────── + Total : 10000.xx ms + ✗ timeout: timed out + +http://192.0.2.1/ (FAILED) + DNS lookup : x.xx ms + TCP connect : 10000.xx ms + ───────────────────────────── + Total : 10000.xx ms + ✗ timeout: timed out +exit: 1 +``` + +--- + +## `tls-errors.txt` — TLS certificate errors + +DNS and TCP complete; TLS handshake is attempted and fails. All three phases +are shown with real timings even though the request fails. + +> **Note:** badssl.com occasionally resets the connection mid-handshake. The +> error may read `[Errno 54] Connection reset by peer` instead of +> `CERTIFICATE_VERIFY_FAILED` — both are classified as `✗ tls`. + +```sh +python phases.py configs/tls-errors.txt +echo "exit: $?" +``` + +Expected output: +``` +https://expired.badssl.com/ (FAILED) + DNS lookup : 15.39 ms + TCP connect : 124.98 ms + TLS handshake : 326.52 ms + ───────────────────────────── + Total : 479.50 ms + ✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired … + +https://self-signed.badssl.com/ (FAILED) + DNS lookup : 2.06 ms + TCP connect : 127.74 ms + TLS handshake : 963.72 ms + ───────────────────────────── + Total : 1103.21 ms + ✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate … + +https://incomplete-chain.badssl.com/ (FAILED) + DNS lookup : 2.11 ms + TCP connect : 2128.40 ms + TLS handshake : 381.42 ms + ───────────────────────────── + Total : 2532.73 ms + ✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer … +exit: 1 +``` + +--- + +## `http-errors.txt` — HTTP 4xx / 5xx status codes + +Unlike `simple.py` (which treats 4xx as FAIL), `phases.py` completes all +phases and shows the raw HTTP status code. The request succeeded at the network +level — you get full phase breakdown plus the 404. + +```sh +python phases.py configs/http-errors.txt +echo "exit: $?" +``` + +Expected output: +``` +https://www.google.com/this-page-does-not-exist-at-all-1234567890 (404) + DNS lookup : 4.12 ms + TCP connect : 10.56 ms + TLS handshake : 25.03 ms + Server (TTFB) : 124.26 ms + Transfer : 0.19 ms + ───────────────────────────── + Total : 172.60 ms + +https://github.com/this-repo-does-not-exist-abcxyz123/no-way (404) + DNS lookup : 16.56 ms + TCP connect : 22.86 ms + TLS handshake : 24.47 ms + Server (TTFB) : 275.52 ms + Transfer : 90.08 ms + ───────────────────────────── + Total : 441.17 ms + +https://www.iana.org/this-page-does-not-exist-either (404) + DNS lookup : 2.48 ms + TCP connect : 10.03 ms + TLS handshake : 16.10 ms + Server (TTFB) : 75.14 ms + Transfer : 0.46 ms + ───────────────────────────── + Total : 115.90 ms +exit: 0 +``` + +> **Compare with `simple.py`:** the same URLs return `FAIL (HTTP Error 404: Not Found)` +> in `simple.py` (because `urlopen` raises for 4xx) but `(404)` with a full +> phase breakdown in `phases.py` (because the request completed at the network level). +> Exit code is `0` here vs `1` in `simple.py`. + +--- + +## `mixed.txt` — one of each (no timeout) + +```sh +python phases.py configs/mixed.txt +echo "exit: $?" +``` + +Expected output: +``` +https://example.com (200) + DNS lookup : 4.88 ms + TCP connect : 11.79 ms + TLS handshake : 17.17 ms + Server (TTFB) : 67.79 ms + Transfer : 0.09 ms + ───────────────────────────── + Total : 108.72 ms + +https://no.such.host.invalid (FAILED) + ───────────────────────────── + Total : 1.46 ms + ✗ dns: [Errno 8] nodename nor servname provided, or not known + +http://127.0.0.1:9999 (FAILED) + DNS lookup : 0.01 ms + TCP connect : 0.82 ms + ───────────────────────────── + Total : 0.85 ms + ✗ connect: [Errno 61] Connection refused + +https://self-signed.badssl.com/ (FAILED) + DNS lookup : 1.54 ms + TCP connect : 1129.98 ms + TLS handshake : 296.93 ms + ───────────────────────────── + Total : 1438.84 ms + ✗ tls: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate … + +https://github.com/this-repo-does-not-exist-abcxyz123/no-way (404) + DNS lookup : 13.48 ms + TCP connect : 22.98 ms + TLS handshake : 24.58 ms + Server (TTFB) : 324.54 ms + Transfer : 95.05 ms + ───────────────────────────── + Total : 495.53 ms +exit: 1 +``` + +Note how the GitHub 404 shows all five phases (network succeeded) while the +DNS, TCP, and TLS failures each show only the phases that ran. + +--- + +## Run all configs + +```sh +# Quick sweep — skips the slow timeout config +for f in configs/*.txt; do + [[ "$f" == *timeout* ]] && continue + echo + echo "=== $f ===" + python phases.py "$f" + echo "exit: $?" +done +``` + +```sh +# Full sweep — includes timeout (~20 s extra) +for f in configs/*.txt; do + echo + echo "=== $f ===" + python phases.py "$f" + echo "exit: $?" +done +``` diff --git a/python/configs/usage.md b/python/configs/usage-simple.md similarity index 100% rename from python/configs/usage.md rename to python/configs/usage-simple.md