AtlatestRenderedmarkdown
Readme

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 #f or 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 passing connect-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 raises HTTP 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-bytes was not bounded by its own timeout argument. 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-download is 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 read timeout: 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 neither timeout: nor connect-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 like Set-Cookie, and it does not follow redirects (a 3xx is returned as-is, which is what a faithful proxy needs). It handles Content-Length, chunked, and connection-close framing, with a single-allocation body assembly. http-request stays the string-oriented convenience path; reach for http-fetch-bytes when the bytes must survive exactly.

[0.18.1] - 2026-07-11

Fixed

  • http-server-stop now actually stops a running serve loop. The server record is immutable — http-server-start and the serve loop derive updated copies — so a caller that held the record it passed to http-server-start never 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 per make-http-server and carried by reference through every derived record; http-server-stop sets it and the loop — whose waits are all deadline-bounded — sees it within the sweep interval, closes the sockets it owns, and http-server-start returns. Stopping an idle server completes in ~100ms (probe-verified; hung forever before). Covered by phase 3 of test/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 via await-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: close after every response. The server honors an explicit Connection: close and 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: chunked and a terminating 0\r\n\r\n, instead of relying on connection close for framing. http-response/file keeps its Content-Length framing (byte-exact).
  • Range: requests and 206 Partial Content. http-response/file accepts a range: keyword (the raw request Range header) and honors byte ranges: bytes=A-B, open-ended bytes=A-, and suffix bytes=-N. Satisfiable ranges yield 206 with Content-Range; unsatisfiable ranges yield 416 Range Not Satisfiable with Content-Range: bytes */<total>; an absent or malformed header falls back to a full 200. Adds the HTTP-RANGE-NOT-SATISFIABLE constant and exports resolve-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 bare http-serve / http-server-start without wrapping the call in with-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 throwing go: not running in an async context on a default server, which caused large assets to intermittently fail to download.
  • HEAD requests now return identical status and headers (including a correct Content-Length) with zero body bytes, instead of sending the full body.

Added

  • Opt-in automatic gzip via a gzip: option on make-http-server / http-serve (default #f, so the default server is unchanged). When enabled, non-streaming responses are gzip-encoded per the request's Accept-Encoding (compressible content types over a size floor), adding Content-Encoding: gzip and Vary: 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/json now accepts either a dict or an already-encoded string.
  • http-response/sse-broadcast defaults 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-gzip response 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