sigil-http / treeCHANGELOG.md
Changelog
All notable changes to sigil-http are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.18.6] - 2026-09-06
Fixed
- Accept bytevector request bodies in
http-fetch-bytes, preserving binary uploads without UTF-8 conversion. Compute Content-Length from the bytevector and replace conflicting length/transfer-encoding headers case-insensitively. Existing string request bodies keep their UTF-8 behavior. - Exercise binary assembly and actual empty/2 MB upload round trips alongside the HTTP regression suite. CI now uses Sigil 0.21.0, matching the test runner's existing minimum dependency, and runs the loopback upload checks with Node.
[0.18.5] - 2026-08-27
Added
http-stream-response: incremental, byte-faithful HTTP response bodies. The callback API publishes the parsed status and ordered response headers once, then publishes raw body bytevectors as they arrive. Content-Length and connection-close bodies are supported, and HTTP/1.1 chunk framing is decoded incrementally across arbitrary transport boundaries. Request construction, TLS, deadlines, and connection handling remain shared with the existing client rather than delegated to an external process.
Fixed
- Framed streaming reads tolerate both non-blocking would-block values. Sigil's socket layer can surface a transient read as either
#for an EOF sentinel. Content-Length and chunked streams now retry both until their framing declares completion or the idle deadline expires.
[0.18.3] - 2026-08-04
Changed
connect-timeout:now bounds the TLS handshake as well as the TCP connect. The connect timeout stopped at the moment the peer accepted, so a server that accepted and then never sent a ServerHello left the handshake read blocking with no way out. Under Sigil's cooperative scheduler that freezes the whole process rather than failing one request, which is how a production monitoring service ran for 55 days while its process, service status and listening port all reported it healthy. Widening the existing keyword rather than adding a new one means every caller already passingconnect-timeout:is fixed by upgrading alone. Each phase gets the full value rather than a shared split, so a slow but working connect cannot consume the handshake's budget. Requires sigil-tls 0.16.5.connect-timeout:now applies to plain HTTP too. It resolves the host and connects to each address under a deadline, giving each address a fair slice of what remains, so a blackholed first address cannot burn the whole budget and leave a working address untried.timeout:now bounds the request write. A request usually fits the socket buffer, so the write usually returns at once, but a peer that never drains its receive queue fills the window and a large body blocks indefinitely. A write that runs out of time raisesHTTP request timed out: write deadline exceeded, deliberately NOT marked ack-unconfirmed: the deadline can only fire with bytes still unsent, so the server holds a partial request it cannot act on and retrying is safe.
Fixed
http-fetch-byteswas not bounded by its owntimeoutargument. Its idle deadline loops detect "no data yet" from an empty read, which only a non-blocking connection returns, and it never set one. Its reads blocked and every deadline check between them was unreachable, so the argument looked like a safeguard and did nothing. The connection is now non-blocking, which is what those loops were written for, and they sleep between retries rather than spinning.
Known limitations
http-downloadis not bounded at all. It takes no timeout keywords and is blocking end to end: connect, write, header read and body stream. It is the API most likely to be pointed at a large, slow remote and it is the one still able to freeze the process. Bounding it means bounding a streaming read too, which is a larger change than this release.timeout:is a per-phase bound, not a total. The write and the read each get the full value, so(http-get url timeout: 5)can spend up to 5 seconds writing and a further 5 reading. Sharing one deadline would have silently shortened the response window existing callers sized their value against; callers who readtimeout:as a total should halve it.- DNS resolution remains unbounded.
getaddrinfo(3)is a blocking call with no deadline and no portable cancellation, and both paths into it are inside native code. A resolver that stops answering still blocks a request for as long as the system resolver takes to give up, and neithertimeout:norconnect-timeout:affects that. Stated here rather than left implicit: "sigil-http bounds its blocking segments" must not be read as "sigil-http cannot block".
[0.18.2] - 2026-07-23
Added
http-fetch-bytes: a byte-faithful, in-memory HTTP fetch. It returns a response as{ status, headers, body }where the body is a raw bytevector and is never decoded to a string, so binary payloads (wasm modules, tarballs, images) round-trip uncorrupted. It preserves header order and duplicates (an ordered alist of lowercased-name/value pairs), so a relay can faithfully forward repeated headers likeSet-Cookie, and it does not follow redirects (a 3xx is returned as-is, which is what a faithful proxy needs). It handlesContent-Length, chunked, and connection-close framing, with a single-allocation body assembly.http-requeststays the string-oriented convenience path; reach forhttp-fetch-byteswhen the bytes must survive exactly.
[0.18.1] - 2026-07-11
Fixed
http-server-stopnow actually stops a running serve loop. The server record is immutable —http-server-startand the serve loop derive updated copies — so a caller that held the record it passed tohttp-server-startnever shared state with the loop, and stopping from another task silently did nothing (the loop kept running forever; with the loop now suspending on the scheduler, it would have kept a periodic wait alive indefinitely). The stop signal now lives in a one-slot vector created permake-http-serverand carried by reference through every derived record;http-server-stopsets it and the loop — whose waits are all deadline-bounded — sees it within the sweep interval, closes the sockets it owns, andhttp-server-startreturns. Stopping an idle server completes in ~100ms (probe-verified; hung forever before). Covered by phase 3 oftest/integration/run-starvation-test.sh.- Server loop no longer starves other async tasks' socket I/O. While at least one client was connected (e.g. a browser holding an SSE stream), the server loop's drain phase busy-spun a short-timeout native
socket-select. Because the async scheduler only polls socket io-waiters when its run queue is empty, that busy loop kept the queue perpetually non-empty and every OTHER task's socket read starved — first reads on outbound connections (IRC-style clients, websocket clients, IPC sockets) were delayed for as long as any HTTP client stayed connected, while timer- and channel-driven work kept running. The drain now suspends on its whole socket set (listen + clients) through the scheduler viaawait-readable-any, waking immediately on readable data and sweeping connection timeouts on a bounded deadline (1s), so sibling io-waiters are serviced within milliseconds. Regression test:test/integration/run-starvation-test.sh(compiled probe; measures a sibling socket's first-read latency with a client parked on the server).
Requires sigil 0.17.10 or newer (the first release providing (sigil async) await-readable-any); the package's sigil: requirement is bumped to ^0.17.10 accordingly.
[0.18.0] - 2026-07-10
Added
- Persistent connections (keep-alive). Non-streaming HTTP/1.1 responses now keep the TCP connection open and serve subsequent requests on the same socket instead of forcing
Connection: closeafter every response. The server honors an explicitConnection: closeand defaults HTTP/1.0 to close; the age-based timeout is refreshed per request so it acts as an idle timeout for kept-alive connections. This removes the connection-churn that amplified image-heavy page loads. - Chunked response transfer-encoding. Streaming responses whose length is unknown (SSE and bare procedure bodies) are now framed with
Transfer-Encoding: chunkedand a terminating0\r\n\r\n, instead of relying on connection close for framing.http-response/filekeeps itsContent-Lengthframing (byte-exact). Range:requests and206 Partial Content.http-response/fileaccepts arange:keyword (the raw requestRangeheader) and honors byte ranges:bytes=A-B, open-endedbytes=A-, and suffixbytes=-N. Satisfiable ranges yield206withContent-Range; unsatisfiable ranges yield416 Range Not SatisfiablewithContent-Range: bytes */<total>; an absent or malformed header falls back to a full200. Adds theHTTP-RANGE-NOT-SATISFIABLEconstant and exportsresolve-range.
Notes
- Reusing a streamed connection for keep-alive is not yet supported — a streaming response still closes when the stream ends (the select loop would need to re-adopt the goroutine-owned socket). HTTP request pipelining is also unsupported. Both are tracked follow-ups.
[0.17.0] - 2026-07-10
Changed
- Streaming responses (
http-response/file, SSE) now work from a barehttp-serve/http-server-startwithout wrapping the call inwith-async. The server establishes its own async scheduler when none is active, and reuses the caller's when one is present. This fixes streaming and SSE routes throwinggo: not running in an async contexton a default server, which caused large assets to intermittently fail to download. HEADrequests now return identical status and headers (including a correctContent-Length) with zero body bytes, instead of sending the full body.
Added
- Opt-in automatic gzip via a
gzip:option onmake-http-server/http-serve(default#f, so the default server is unchanged). When enabled, non-streaming responses are gzip-encoded per the request'sAccept-Encoding(compressible content types over a size floor), addingContent-Encoding: gzipandVary: Accept-Encoding. Streaming and incompressible bodies are left untouched.
[0.16.7] - 2026-07-07
Changed
- Realigned the sigil toolchain pin to
^0.17. http-response/jsonnow accepts either a dict or an already-encoded string.http-response/sse-broadcastdefaults its message handler to identity.
[0.16.6] - 2026-06-30
Fixed
- HTTP client
timeout:callers can now distinguish a request that was never sent from one that was delivered but whose ack is unconfirmed.
[0.16.5] - 2026-06-30
Changed
- Threaded a
connect-timeout:option through the HTTP client and relocked onto sigil-tls 0.16.2.
[0.16.4] - 2026-06-30
Fixed
- Response reads now terminate on HTTP framing rather than on connection close.
[0.16.3] - 2026-06-29
Added
- Opt-in
timeout:keyword for HTTP client reads.
[0.16.2] - 2026-06-24
Changed
- Repointed sigil-test dev-dependencies at the reintegrated monorepo.
[0.16.1] - 2026-05-31
Added
http-response-gzipresponse helper (gzip content negotiation).
Changed
- Response send loop rewritten to be O(n) via offset-based partial writes, avoiding O(n²) recopying of large bodies over slow links.
[0.16.0] - 2026-05-06
Initial tagged release of the standalone sigil-http package, extracted from the sigil monorepo (requires sigil ^0.16).
Fixed
- Non-blocking HTTP response writes.
- HTTP request body byte handling (Content-Length counted in bytes).
Added
- Server crash diagnostics and guard blocks around the handler, streaming goroutine, and server loop; an outer server-loop guard that restarts on an otherwise-escaping exception.
- SSE heartbeat utility for dead-client detection.
[0.17.0]: https://codeberg.org/sigil/sigil-http/compare/v0.16.7...v0.17.0 [0.16.7]: https://codeberg.org/sigil/sigil-http/compare/v0.16.6...v0.16.7 [0.16.6]: https://codeberg.org/sigil/sigil-http/compare/v0.16.5...v0.16.6 [0.16.5]: https://codeberg.org/sigil/sigil-http/compare/v0.16.4...v0.16.5 [0.16.4]: https://codeberg.org/sigil/sigil-http/compare/v0.16.3...v0.16.4 [0.16.3]: https://codeberg.org/sigil/sigil-http/compare/v0.16.2...v0.16.3 [0.16.2]: https://codeberg.org/sigil/sigil-http/compare/v0.16.1...v0.16.2 [0.16.1]: https://codeberg.org/sigil/sigil-http/compare/v0.16.0...v0.16.1 [0.16.0]: https://codeberg.org/sigil/sigil-http/releases/tag/v0.16.0