AtlatestRepositorysigil-http

sigil-http / treeRELEASE_NOTES_v0.18.3.md

Read it rendered

1Bound the TLS handshake and the request write, so an outbound call cannot block forever
2
3An outbound HTTP call passes through six blocking segments. Four of them were
4unbounded, including the one that matters most: the TLS handshake. Once a peer
5has accepted the TCP connection the connect phase is over, so `connect-timeout:`
6never reached the handshake read that follows. A server that accepts and then
7never sends a ServerHello left that read blocking with no way out, and because
8Sigil's scheduler is cooperative that does not fail one request, it freezes the
9whole process.
11That is not a hypothetical. It ran a production monitoring service for 55 days
12while its process, its service status and its listening port all reported it
13healthy.
15**Name resolution is still unbounded, and this release does not change that.**
16See "Known limitations" below. Please do not read "sigil-http bounds its
17blocking segments" as "sigil-http cannot block".
19## Who this fixes without a code change
21`connect-timeout:` now bounds the TLS handshake as well as the TCP connect, so
22**anything already passing `connect-timeout:` is fixed by upgrading alone**. In
23this estate that is courier, on both its Telegram poll and send paths, through
24sigil-telegram's `connect-timeout` field.
26Everything else has to start passing it. A survey of the estate's repositories
27found no other caller supplying `connect-timeout:` today, so vigil, kiln,
28bureau, tally and fjo each need to add it to get the protection.
30## Changes
32- `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 but
34 working connect cannot consume the handshake's budget. The worst case before
35 a request is sent is therefore about twice `connect-timeout:`.
36- `connect-timeout:` now works for plain HTTP as well. It resolves the host and
37 connects to each address under a deadline, giving each address a fair slice of
38 what remains, so a blackholed first address cannot burn the whole budget and
39 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 receive
42 queue fills the window and a large body then blocks indefinitely. A write that
43 runs out of time raises `HTTP request timed out: write deadline exceeded`.
44 That error is deliberately NOT marked ack-unconfirmed: a write deadline can
45 only fire with bytes still unsent, so the server holds a partial request it
46 cannot act on and retrying is safe.
47- The response read keeps its own fresh deadline rather than sharing the
48 write's, so the response window that `timeout:` has always meant is
49 unchanged. **The consequence is that `timeout:` is now a per-phase bound
50 rather than a total**: `(http-get url timeout: 5)` can spend up to 5 seconds
51 writing and a further 5 reading. Sharing one deadline would have silently
52 shortened the response window that existing callers sized their value
53 against, which seemed the worse of the two changes, but callers who read
54 `timeout:` as a total should halve it.
56## Fixed
58- `http-fetch-bytes` was not bounded by its own `timeout` argument. Its idle
59 deadline loops detect "no data yet" from an empty read, which only a
60 non-blocking connection ever returns, and it never set one. So its reads
61 blocked and every deadline check between them was unreachable: the argument
62 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 retries
64 instead of spinning.
66## Known limitations
68- **`http-download` is not bounded at all.** It takes no timeout keywords and
69 is blocking end to end: connect, write, header read and body stream. It is
70 the API most likely to be pointed at a large, slow remote, and it is the one
71 still able to freeze the process. Bounding it means bounding a streaming
72 read as well, which is a larger change than this release. Stated here rather
73 than left to be discovered.
75- **DNS resolution is unbounded.** `getaddrinfo(3)` is a blocking call with no
76 deadline argument and no portable cancellation, and both paths into it are
77 inside native code: the bounded plain-HTTP connect must resolve before it can
78 connect per-address, and `tls-connect` resolves inside the same native call
79 that performs its bounded connect. A resolver that stops answering still
80 blocks a request for as long as the system resolver takes to give up, and
81 neither `timeout:` nor `connect-timeout:` affects that.
83 Callers needing a hard bound across resolution too have to get it outside
84 this library, by running the request somewhere it can be abandoned, or by
85 resolving ahead of time and requesting by address. Fixing it properly means
86 adopting the async resolver that sigil-socket 0.17 added, which raises the
87 required floor for every consumer and is a separate change.
89## Upgrade notes
91Requires sigil-tls 0.16.5, which carries the handshake bound itself. The pin is
92updated accordingly.
94Every bound remains opt-in. With `timeout:` and `connect-timeout:` omitted the
95behaviour is exactly what it was: blocking connect, blocking write, blocking
96read. Nothing changes for a caller that passes neither.