- Rename python/configs/usage.md → usage-simple.md so both scripts have a parallel runnable reference file in the same directory - Add python/configs/usage-phases.md: runnable sh commands + real expected output for phases.py against every error-path config, including a note on the key difference from simple.py (HTTP 4xx exits 0 in phases.py vs 1 in simple.py because phases.py completes at the network level) - Update doc cross-links in docs/usage/py-simple.md and py-phases.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
348 lines
10 KiB
Markdown
348 lines
10 KiB
Markdown
# `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
|
|
```
|