# What a failed POST says about whether it arrived

Probe: `python3 -I probe.py` (Python 3.12.3, Linux 6.8, loopback, plain HTTP). A local server
accepts one connection and either resets it at once (`immediate`: SO_LINGER 0, nothing read),
reads the whole request and then resets (`after_read`), or reads it and closes cleanly
(`close_after_read`); a port with no listener gives `refused`. The client is `urllib.request.urlopen`.

Run 2026-10-27 17:32 Rome, 20 repetitions of the 200-byte POST per mode:

| server did | exception seen by the client | count |
|---|---|---|
| reset before reading a byte | `ConnectionResetError` (bare) | 13 / 20 |
| reset before reading a byte | `URLError(reason=ConnectionResetError)` | 7 / 20 |
| read the whole request, then reset | `ConnectionResetError` (bare) | 20 / 20 |
| read the whole request, then closed | `RemoteDisconnected` | 1 / 1 |
| nobody listening | `URLError(reason=ConnectionRefusedError)` | 1 / 1 |

With a 4 MB body the immediate reset surfaced as `URLError` (the send itself failed).

Reading: urllib wraps an OSError raised inside `h.request()` (connect, TLS, send) in URLError and lets
one raised inside `h.getresponse()` through bare. A small body fits in the kernel's send buffer, so
`request()` usually returns before the peer's reset arrives, and the reset surfaces while waiting for
the answer whether or not the server read anything. So a bare reset is consistent with both "never
read" and "read in full"; a refusal is the only failure that proves nothing was sent. Over HTTPS a
reset during the TLS handshake happens inside `request()` and is wrapped. Loopback only; a real
network adds timing but not a new kind of evidence.

Context: Hesper's turn seals (hesper/seal.py); 11 of turns 2-102 lost their registry seal to
"[Errno 104] Connection reset by peer", and the stored note keeps only that text, not the wrapper.
