streamhttp
Tiny synchronous streaming HTTP/1.1 client. Reads chunked bodies as they arrive.
Summary
| Latest Version | 0.4.5 |
|---|---|
| License | MIT |
| CI Status | Failing |
| Downloads | 0 |
| Last Indexed | 2026-09-05 07:26 |
Tags
Authors
- capocasa
Installation
nimble install streamhttp
choosenim install streamhttp
git clone https://github.com/capocasa/streamhttp
OS Compatibility
| Platform | Linux | macOS | Windows | FreeBSD | OpenBSD | NetBSD | Android | iOS | WASM | Embedded |
|---|---|---|---|---|---|---|---|---|---|---|
| streamhttp | ✓ | ✓ | ✓ | - | - | - | - | - | - | - |
Dependencies
| Package | Version | Optional |
|---|---|---|
| nim >= | 2.0.0 | No |
Source
| Repository | https://github.com/capocasa/streamhttp |
|---|---|
| Homepage | https://github.com/capocasa/streamhttp |
| Registry Source | nimble_official |
README
streamhttp
Tiny synchronous streaming HTTP/1.1 client for Nim. Reads response bodies as they arrive — chunked transfer encoding, content-length, connection-close — without an async runtime, without a subprocess, without buffering the body up front.
For when you wanted to consume Server-Sent Events from a sync call site
and Nim's std/httpclient was busy slurping the whole body before
giving you anything.
Why?
Nim's stdlib HttpClient calls parseBody synchronously inside
request, which means the whole body is read before the call returns.
Fine for one-shots, useless for SSE. The async client streams via
FutureStream but drags std/asyncdispatch into your code. Subprocess
curl works but forks a process per request and you parse stdout
markers.
This module reads bytes off a net.Socket (TLS or plain), runs them
through a tiny chunked-encoding state machine, and yields one body line
at a time as data trickles in. Block on recv like any sync client.
Usage
import streamhttp
let c = connectTls("api.openai.com")
defer: c.close()
c.sendRequest("POST", "/v1/chat/completions", "api.openai.com",
headers = {"Authorization": "Bearer " & key,
"Content-Type": "application/json",
"Accept": "text/event-stream"},
body = jsonBody)
let resp = c.readResponseHead()
echo "status: ", resp.status
for line in c.lines:
if line.startsWith("data: "):
let payload = line["data: ".len .. ^1]
if payload == "[DONE]": break
process(payload) # parse SSE event as it arrives
Plain HTTP for testing or http:// endpoints:
let c = connectPlain("127.0.0.1", Port(8080))
Already-connected socket (BYO TLS):
let c = newStreamConn(mySocket, mySslCtx)
Decoder API
The chunked-encoding decoder is exposed separately for testing or for consumers that drive their own I/O:
var d = initBodyDecoder(beChunked)
d.feed(rawBytes)
var buf = ""
case d.decode(buf)
of drBytes: # buf has body bytes, may have more
of drNeedMore: # feed more rawBytes
of drDone: # body fully consumed (any final bytes in buf)
of drError: # malformed input
Three encodings supported:
beIdentitywithcontentLength >= 0— sized bodybeIdentitywithcontentLength < 0— read until socket close (HTTP/1.0 style); callmarkEofwhen the socket closesbeChunked—Transfer-Encoding: chunkedper RFC 7230, ignoring chunk extensions and discarding trailer headers
Closing a connection
close never blocks: it skips the TLS close_notify, frees the
OpenSSL handle, sets SO_LINGER=0, and closes the fd directly. It is
idempotent and may be called from any thread.
One ordering rule is load-bearing: close frees the SSL handle and fd
immediately, so a concurrent in-flight recv on the same StreamConn
is use-after-free territory. If another thread might be blocked in
readResponseHead/readLine/lines, first interrupt the read:
discard posix.shutdown(c.getFd, SHUT_RDWR) # reader's recv returns
# reader observes its cancel condition and unwinds out of the read path
c.close()
If the conn is only ever touched by one thread (the common
case, including defer: c.close()), no shutdown is needed.
DNS
getAddrInfo has no timeout, so each host is resolved once per
process and the first usable IP is cached; connects then go by IP
(SNI and cert validation still use the original hostname). Call
invalidateResolved(host, port) after a connect failure so a stale
record pointing at a dead host is re-resolved on the next attempt.
The single first resolve per host is still unbounded (the libc resolver can hang on a black-holed DNS path). That is the documented floor; bounding it needs a resolver thread or c-ares, deliberately out of scope here.
Scope
What's in: HTTP/1.1, TLS via std/net, sized + chunked + until-close
bodies, line-buffered API on top, connection keep-alive (no special
handling — just don't close between requests on the same StreamConn).
What's not: HTTP/2, redirects, automatic decompression, request streaming, cookies, proxy support. If you want any of those, reach for chronos or roll your own on top of this.
Install
nimble install streamhttp
Requires -d:ssl for TLS support (Nim convention).
License
MIT.