Streaming HTTP responses — Server-Sent Events, portable.
koine.http/request buffers: it hands you the whole body once the server is
done. That is wrong for an LLM, where the point of stream mode is that
tokens surface as they are produced. This namespace is the incremental
counterpart, kept in its own file because the host seams are completely
different from the buffered ones (a buffered client and a streaming client
share a URL and nothing else).
The contract is INCREMENTAL DELIVERY, not "returns the same lines". A host that can only buffer does not get a slow-but-correct fallback here — it throws (rule 2). A fake stream passes every test and then fails in front of a user, which is strictly worse than an honest error.
Measured on 2026-07-27 (150 ms between server events, arrival times relative to the first byte):
| host | streams? | route |
|---|---|---|
| JVM | yes | HttpResponse$BodyHandlers/ofInputStream |
| let-go | yes | (http/request {… :as :stream}) + io/line-seq |
| Glojure | yes | net/http + chunked Body.Read into bytes.Buffer |
| cljgo | yes | (cljg.net.http/request {… :as :stream}) + cljg.stream/read-line |
The cljgo row was no until 2026-07-30 — cljg.net.http buffered through
io.ReadAll and exposed no reader. The :as :stream shim closed it.
Do NOT reach for BodyHandlers/ofLines on the JVM. It looks like the clean
route and it is not — see the comment on the :clj branch.
Streaming HTTP responses — Server-Sent Events, portable.
`koine.http/request` buffers: it hands you the whole body once the server is
done. That is wrong for an LLM, where the point of `stream` mode is that
tokens surface *as they are produced*. This namespace is the incremental
counterpart, kept in its own file because the host seams are completely
different from the buffered ones (a buffered client and a streaming client
share a URL and nothing else).
The contract is INCREMENTAL DELIVERY, not "returns the same lines". A host
that can only buffer does **not** get a slow-but-correct fallback here — it
throws (rule 2). A fake stream passes every test and then fails in front of a
user, which is strictly worse than an honest error.
Measured on 2026-07-27 (150 ms between server events, arrival times relative
to the first byte):
| host | streams? | route |
|---------|----------|---------------------------------------------------|
| JVM | yes | `HttpResponse$BodyHandlers/ofInputStream` |
| let-go | yes | `(http/request {… :as :stream})` + `io/line-seq` |
| Glojure | yes | `net/http` + chunked `Body.Read` into `bytes.Buffer` |
| cljgo | yes | `(cljg.net.http/request {… :as :stream})` + `cljg.stream/read-line` |
The cljgo row was `no` until 2026-07-30 — `cljg.net.http` buffered through
`io.ReadAll` and exposed no reader. The `:as :stream` shim closed it.
Do NOT reach for `BodyHandlers/ofLines` on the JVM. It looks like the clean
route and it is not — see the comment on the `:clj` branch.(parse-sse-line line)Parse one raw SSE line.
-> {:event "delta" :data nil} for event: delta
-> {:event nil :data "{…}"} for data: {…}
-> nil for blank lines, comments (: ping), and
fields this shape cannot carry (id,
retry, unknown).
A trailing CR and/or LF is stripped, so it accepts a line either with or without its terminator. Pure; identical on every host.
Parse one raw SSE line.
-> {:event "delta" :data nil} for `event: delta`
-> {:event nil :data "{…}"} for `data: {…}`
-> nil for blank lines, comments (`: ping`), and
fields this shape cannot carry (`id`,
`retry`, unknown).
A trailing CR and/or LF is stripped, so it accepts a line either with or
without its terminator. Pure; identical on every host.(sse-post url headers body on-event)POST body to url and invoke (on-event data-string) once per SSE
data: line, AS IT ARRIVES. Returns {:status n} when the stream ends.
[DONE] is delivered like any other datum — the SSE framing has no idea it
is a sentinel, and deciding that is the caller's job, not the transport's.
Blocks until the server closes the stream. Header values are passed through verbatim and never logged; they routinely carry credentials.
Throws on a host with no incremental route. It does not quietly buffer.
POST `body` to `url` and invoke `(on-event data-string)` once per SSE
`data:` line, AS IT ARRIVES. Returns `{:status n}` when the stream ends.
`[DONE]` is delivered like any other datum — the SSE framing has no idea it
is a sentinel, and deciding that is the caller's job, not the transport's.
Blocks until the server closes the stream. Header values are passed through
verbatim and never logged; they routinely carry credentials.
Throws on a host with no incremental route. It does not quietly buffer.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |