AtlatestRepositorysigil-http
sigil-http / treeRELEASE_NOTES_v0.18.3.md
1
Bound the TLS handshake and the request write, so an outbound call cannot block forever3
An outbound HTTP call passes through six blocking segments. Four of them were4
unbounded, including the one that matters most: the TLS handshake. Once a peer5
has accepted the TCP connection the connect phase is over, so `connect-timeout:`6
never reached the handshake read that follows. A server that accepts and then7
never sends a ServerHello left that read blocking with no way out, and because8
Sigil's scheduler is cooperative that does not fail one request, it freezes the9
whole process.11
That is not a hypothetical. It ran a production monitoring service for 55 days12
while its process, its service status and its listening port all reported it13
healthy.15
**Name resolution is still unbounded, and this release does not change that.**16
See "Known limitations" below. Please do not read "sigil-http bounds its17
blocking segments" as "sigil-http cannot block".19
## Who this fixes without a code change21
`connect-timeout:` now bounds the TLS handshake as well as the TCP connect, so22
**anything already passing `connect-timeout:` is fixed by upgrading alone**. In23
this estate that is courier, on both its Telegram poll and send paths, through24
sigil-telegram's `connect-timeout` field.26
Everything else has to start passing it. A survey of the estate's repositories27
found no other caller supplying `connect-timeout:` today, so vigil, kiln,28
bureau, tally and fjo each need to add it to get the protection.30
## Changes32
- `connect-timeout:` bounds the TLS handshake in addition to the TCP connect.33
Each phase gets the full value rather than a shared split, so a slow but34
working connect cannot consume the handshake's budget. The worst case before35
a request is sent is therefore about twice `connect-timeout:`.36
- `connect-timeout:` now works for plain HTTP as well. It resolves the host and37
connects to each address under a deadline, giving each address a fair slice of38
what remains, so a blackholed first address cannot burn the whole budget and39
leave a working address untried.40
- `timeout:` bounds the request write. A request usually fits the socket buffer,41
so the write usually returns at once, but a peer that never drains its receive42
queue fills the window and a large body then blocks indefinitely. A write that43
runs out of time raises `HTTP request timed out: write deadline exceeded`.44
That error is deliberately NOT marked ack-unconfirmed: a write deadline can45
only fire with bytes still unsent, so the server holds a partial request it46
cannot act on and retrying is safe.47
- The response read keeps its own fresh deadline rather than sharing the48
write's, so the response window that `timeout:` has always meant is49
unchanged. **The consequence is that `timeout:` is now a per-phase bound50
rather than a total**: `(http-get url timeout: 5)` can spend up to 5 seconds51
writing and a further 5 reading. Sharing one deadline would have silently52
shortened the response window that existing callers sized their value53
against, which seemed the worse of the two changes, but callers who read54
`timeout:` as a total should halve it.56
## Fixed58
- `http-fetch-bytes` was not bounded by its own `timeout` argument. Its idle59
deadline loops detect "no data yet" from an empty read, which only a60
non-blocking connection ever returns, and it never set one. So its reads61
blocked and every deadline check between them was unreachable: the argument62
looked like a safeguard and did nothing. The connection is now non-blocking,63
which is what those loops were written for, and they sleep between retries64
instead of spinning.66
## Known limitations68
- **`http-download` is not bounded at all.** It takes no timeout keywords and69
is blocking end to end: connect, write, header read and body stream. It is70
the API most likely to be pointed at a large, slow remote, and it is the one71
still able to freeze the process. Bounding it means bounding a streaming72
read as well, which is a larger change than this release. Stated here rather73
than left to be discovered.75
- **DNS resolution is unbounded.** `getaddrinfo(3)` is a blocking call with no76
deadline argument and no portable cancellation, and both paths into it are77
inside native code: the bounded plain-HTTP connect must resolve before it can78
connect per-address, and `tls-connect` resolves inside the same native call79
that performs its bounded connect. A resolver that stops answering still80
blocks a request for as long as the system resolver takes to give up, and81
neither `timeout:` nor `connect-timeout:` affects that.83
Callers needing a hard bound across resolution too have to get it outside84
this library, by running the request somewhere it can be abandoned, or by85
resolving ahead of time and requesting by address. Fixing it properly means86
adopting the async resolver that sigil-socket 0.17 added, which raises the87
required floor for every consumer and is a separate change.89
## Upgrade notes91
Requires sigil-tls 0.16.5, which carries the handshake bound itself. The pin is92
updated accordingly.94
Every bound remains opt-in. With `timeout:` and `connect-timeout:` omitted the95
behaviour is exactly what it was: blocking connect, blocking write, blocking96
read. Nothing changes for a caller that passes neither.