AtlatestRepositorysigil-http
sigil-http / treeCHANGELOG.md
1
# Changelog3
All notable changes to **sigil-http** are documented in this file.5
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),6
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).8
## [0.18.6] - 2026-09-0610
### Fixed12
- Accept bytevector request bodies in `http-fetch-bytes`, preserving binary13
uploads without UTF-8 conversion. Compute Content-Length from the bytevector14
and replace conflicting length/transfer-encoding headers case-insensitively.15
Existing string request bodies keep their UTF-8 behavior.16
- Exercise binary assembly and actual empty/2 MB upload round trips alongside17
the HTTP regression suite. CI now uses Sigil 0.21.0, matching the test runner's18
existing minimum dependency, and runs the loopback upload checks with Node.20
## [0.18.5] - 2026-08-2722
### Added24
- **`http-stream-response`: incremental, byte-faithful HTTP response bodies.**25
The callback API publishes the parsed status and ordered response headers26
once, then publishes raw body bytevectors as they arrive. Content-Length and27
connection-close bodies are supported, and HTTP/1.1 chunk framing is decoded28
incrementally across arbitrary transport boundaries. Request construction,29
TLS, deadlines, and connection handling remain shared with the existing30
client rather than delegated to an external process.32
### Fixed34
- **Framed streaming reads tolerate both non-blocking would-block values.**35
Sigil's socket layer can surface a transient read as either `#f` or an EOF36
sentinel. Content-Length and chunked streams now retry both until their37
framing declares completion or the idle deadline expires.39
## [0.18.3] - 2026-08-0441
### Changed43
- **`connect-timeout:` now bounds the TLS handshake as well as the TCP44
connect.** The connect timeout stopped at the moment the peer accepted, so a45
server that accepted and then never sent a ServerHello left the handshake46
read blocking with no way out. Under Sigil's cooperative scheduler that47
freezes the whole process rather than failing one request, which is how a48
production monitoring service ran for 55 days while its process, service49
status and listening port all reported it healthy. Widening the existing50
keyword rather than adding a new one means every caller already passing51
`connect-timeout:` is fixed by upgrading alone. Each phase gets the full52
value rather than a shared split, so a slow but working connect cannot53
consume the handshake's budget. Requires sigil-tls 0.16.5.54
- **`connect-timeout:` now applies to plain HTTP too.** It resolves the host55
and connects to each address under a deadline, giving each address a fair56
slice of what remains, so a blackholed first address cannot burn the whole57
budget and leave a working address untried.58
- **`timeout:` now bounds the request write.** A request usually fits the59
socket buffer, so the write usually returns at once, but a peer that never60
drains its receive queue fills the window and a large body blocks61
indefinitely. A write that runs out of time raises `HTTP request timed out:62
write deadline exceeded`, deliberately NOT marked ack-unconfirmed: the63
deadline can only fire with bytes still unsent, so the server holds a partial64
request it cannot act on and retrying is safe.66
### Fixed68
- **`http-fetch-bytes` was not bounded by its own `timeout` argument.** Its69
idle deadline loops detect "no data yet" from an empty read, which only a70
non-blocking connection returns, and it never set one. Its reads blocked and71
every deadline check between them was unreachable, so the argument looked72
like a safeguard and did nothing. The connection is now non-blocking, which73
is what those loops were written for, and they sleep between retries rather74
than spinning.76
### Known limitations78
- **`http-download` is not bounded at all.** It takes no timeout keywords and79
is blocking end to end: connect, write, header read and body stream. It is80
the API most likely to be pointed at a large, slow remote and it is the one81
still able to freeze the process. Bounding it means bounding a streaming82
read too, which is a larger change than this release.83
- **`timeout:` is a per-phase bound, not a total.** The write and the read84
each get the full value, so `(http-get url timeout: 5)` can spend up to 585
seconds writing and a further 5 reading. Sharing one deadline would have86
silently shortened the response window existing callers sized their value87
against; callers who read `timeout:` as a total should halve it.88
- **DNS resolution remains unbounded.** `getaddrinfo(3)` is a blocking call89
with no deadline and no portable cancellation, and both paths into it are90
inside native code. A resolver that stops answering still blocks a request91
for as long as the system resolver takes to give up, and neither `timeout:`92
nor `connect-timeout:` affects that. Stated here rather than left implicit:93
"sigil-http bounds its blocking segments" must not be read as "sigil-http94
cannot block".97
## [0.18.2] - 2026-07-2399
### Added101
- **`http-fetch-bytes`: a byte-faithful, in-memory HTTP fetch.** It returns a102
response as `{ status, headers, body }` where the body is a raw bytevector and103
is never decoded to a string, so binary payloads (wasm modules, tarballs,104
images) round-trip uncorrupted. It preserves header order and duplicates (an105
ordered alist of lowercased-name/value pairs), so a relay can faithfully106
forward repeated headers like `Set-Cookie`, and it does not follow redirects107
(a 3xx is returned as-is, which is what a faithful proxy needs). It handles108
`Content-Length`, chunked, and connection-close framing, with a109
single-allocation body assembly. `http-request` stays the string-oriented110
convenience path; reach for `http-fetch-bytes` when the bytes must survive111
exactly.114
## [0.18.1] - 2026-07-11116
### Fixed118
- **`http-server-stop` now actually stops a running serve loop.** The server119
record is immutable — `http-server-start` and the serve loop derive updated120
copies — so a caller that held the record it passed to `http-server-start`121
never shared state with the loop, and stopping from another task silently122
did nothing (the loop kept running forever; with the loop now suspending on123
the scheduler, it would have kept a periodic wait alive indefinitely). The124
stop signal now lives in a one-slot vector created per `make-http-server`125
and carried by reference through every derived record; `http-server-stop`126
sets it and the loop — whose waits are all deadline-bounded — sees it127
within the sweep interval, closes the sockets it owns, and128
`http-server-start` returns. Stopping an idle server completes in ~100ms129
(probe-verified; hung forever before). Covered by phase 3 of130
`test/integration/run-starvation-test.sh`.132
- **Server loop no longer starves other async tasks' socket I/O.** While at133
least one client was connected (e.g. a browser holding an SSE stream), the134
server loop's drain phase busy-spun a short-timeout native `socket-select`.135
Because the async scheduler only polls socket io-waiters when its run queue136
is empty, that busy loop kept the queue perpetually non-empty and every137
OTHER task's socket read starved — first reads on outbound connections138
(IRC-style clients, websocket clients, IPC sockets) were delayed for as long139
as any HTTP client stayed connected, while timer- and channel-driven work140
kept running. The drain now suspends on its whole socket set141
(listen + clients) through the scheduler via `await-readable-any`, waking142
immediately on readable data and sweeping connection timeouts on a bounded143
deadline (1s), so sibling io-waiters are serviced within milliseconds.144
Regression test: `test/integration/run-starvation-test.sh` (compiled probe;145
measures a sibling socket's first-read latency with a client parked on the146
server).148
**Requires** sigil 0.17.10 or newer (the first release providing149
`(sigil async)` `await-readable-any`); the package's `sigil:` requirement is150
bumped to `^0.17.10` accordingly.152
## [0.18.0] - 2026-07-10154
### Added156
- **Persistent connections (keep-alive).** Non-streaming HTTP/1.1 responses now157
keep the TCP connection open and serve subsequent requests on the same socket158
instead of forcing `Connection: close` after every response. The server honors159
an explicit `Connection: close` and defaults HTTP/1.0 to close; the age-based160
timeout is refreshed per request so it acts as an idle timeout for kept-alive161
connections. This removes the connection-churn that amplified image-heavy page162
loads.163
- **Chunked response transfer-encoding.** Streaming responses whose length is164
unknown (SSE and bare procedure bodies) are now framed with165
`Transfer-Encoding: chunked` and a terminating `0\r\n\r\n`, instead of relying166
on connection close for framing. `http-response/file` keeps its167
`Content-Length` framing (byte-exact).168
- **`Range:` requests and `206 Partial Content`.** `http-response/file` accepts169
a `range:` keyword (the raw request `Range` header) and honors byte ranges:170
`bytes=A-B`, open-ended `bytes=A-`, and suffix `bytes=-N`. Satisfiable ranges171
yield `206` with `Content-Range`; unsatisfiable ranges yield `416 Range Not172
Satisfiable` with `Content-Range: bytes */<total>`; an absent or malformed173
header falls back to a full `200`. Adds the `HTTP-RANGE-NOT-SATISFIABLE`174
constant and exports `resolve-range`.176
### Notes178
- Reusing a **streamed** connection for keep-alive is not yet supported — a179
streaming response still closes when the stream ends (the select loop would180
need to re-adopt the goroutine-owned socket). HTTP request pipelining is also181
unsupported. Both are tracked follow-ups.183
## [0.17.0] - 2026-07-10185
### Changed187
- Streaming responses (`http-response/file`, SSE) now work from a bare188
`http-serve` / `http-server-start` **without** wrapping the call in189
`with-async`. The server establishes its own async scheduler when none is190
active, and reuses the caller's when one is present. This fixes streaming and191
SSE routes throwing `go: not running in an async context` on a default192
server, which caused large assets to intermittently fail to download.193
- `HEAD` requests now return identical status and headers (including a correct194
`Content-Length`) with **zero body bytes**, instead of sending the full body.196
### Added198
- Opt-in automatic gzip via a `gzip:` option on `make-http-server` /199
`http-serve` (default `#f`, so the default server is unchanged). When enabled,200
non-streaming responses are gzip-encoded per the request's `Accept-Encoding`201
(compressible content types over a size floor), adding `Content-Encoding:202
gzip` and `Vary: Accept-Encoding`. Streaming and incompressible bodies are203
left untouched.205
## [0.16.7] - 2026-07-07207
### Changed209
- Realigned the sigil toolchain pin to `^0.17`.210
- `http-response/json` now accepts either a dict or an already-encoded string.211
- `http-response/sse-broadcast` defaults its message handler to identity.213
## [0.16.6] - 2026-06-30215
### Fixed217
- HTTP client `timeout:` callers can now distinguish a request that was never218
sent from one that was delivered but whose ack is unconfirmed.220
## [0.16.5] - 2026-06-30222
### Changed224
- Threaded a `connect-timeout:` option through the HTTP client and relocked onto225
sigil-tls 0.16.2.227
## [0.16.4] - 2026-06-30229
### Fixed231
- Response reads now terminate on HTTP framing rather than on connection close.233
## [0.16.3] - 2026-06-29235
### Added237
- Opt-in `timeout:` keyword for HTTP client reads.239
## [0.16.2] - 2026-06-24241
### Changed243
- Repointed sigil-test dev-dependencies at the reintegrated monorepo.245
## [0.16.1] - 2026-05-31247
### Added249
- `http-response-gzip` response helper (gzip content negotiation).251
### Changed253
- Response send loop rewritten to be O(n) via offset-based partial writes,254
avoiding O(n²) recopying of large bodies over slow links.256
## [0.16.0] - 2026-05-06258
Initial tagged release of the standalone `sigil-http` package, extracted from259
the sigil monorepo (requires sigil `^0.16`).261
### Fixed263
- Non-blocking HTTP response writes.264
- HTTP request body byte handling (Content-Length counted in bytes).266
### Added268
- Server crash diagnostics and guard blocks around the handler, streaming269
goroutine, and server loop; an outer server-loop guard that restarts on an270
otherwise-escaping exception.271
- SSE heartbeat utility for dead-client detection.273
[0.17.0]: https://codeberg.org/sigil/sigil-http/compare/v0.16.7...v0.17.0274
[0.16.7]: https://codeberg.org/sigil/sigil-http/compare/v0.16.6...v0.16.7275
[0.16.6]: https://codeberg.org/sigil/sigil-http/compare/v0.16.5...v0.16.6276
[0.16.5]: https://codeberg.org/sigil/sigil-http/compare/v0.16.4...v0.16.5277
[0.16.4]: https://codeberg.org/sigil/sigil-http/compare/v0.16.3...v0.16.4278
[0.16.3]: https://codeberg.org/sigil/sigil-http/compare/v0.16.2...v0.16.3279
[0.16.2]: https://codeberg.org/sigil/sigil-http/compare/v0.16.1...v0.16.2280
[0.16.1]: https://codeberg.org/sigil/sigil-http/compare/v0.16.0...v0.16.1281
[0.16.0]: https://codeberg.org/sigil/sigil-http/releases/tag/v0.16.0