Skip to content

Stream the HttpURLConnection body off the wire - #37

Merged
yogthos merged 4 commits into
mainfrom
stream-urlconnection-body
Oct 13, 2026
Merged

yogthos merged 4 commits into
mainfrom
stream-urlconnection-body

Conversation

@yogthos

@yogthos yogthos commented Oct 13, 2026

Copy link
Copy Markdown
Member

clj-http-lite's :as :stream returned only after the whole body had arrived: the HttpURLConnection shim read the response buffered, so an SSE feed came back in one piece at the end. The shim now hands back a live body the same way the java.net.http path does, and returns at the headers. The connection is pooled once the body is read to its end and closed when the stream is closed early, on disconnect, or when a read fails. Every other :as drains the same stream, so decoding is unchanged. openStream now throws on an error status like the JVM's.

Measured against a server sending one chunk a second for 3 s: headers at 3 ms and a read per chunk, where it was headers and the whole body at 3011 ms.

Also:

  • The persistent test server closed each connection twice; the second close hit a reused fd in the next test.
  • CI runs on jolt's nightly build instead of the 0.8.16 pin. The floor in deps.edn is unchanged but no longer tested.
  • The TLS release tests counted every close in the process, including the test server's own, which failed on Linux under nightly.

clj-http-lite's `:as :stream` did not return until the whole body had arrived:
perform! read the response buffered, so getInputStream handed back a stream
over a body already read to EOF and an SSE feed came out in one piece at the
end. perform! now asks read-response for a live body, the same way the
java.net.http path does, and getInputStream / getErrorStream wrap its pull in
make-body-stream. The connection goes back to the pool when the body is read
to its end and is closed when the stream is closed early, on disconnect, or
when a read fails. Redirect hops drain their body before following.

The socket read timeout stays armed for the body here, which is
HttpURLConnection's contract and what lets a reader bound the gap between
chunks. Every other `:as` drains the same stream, so gzip/deflate decoding is
unchanged. openStream now throws on an error status like the JVM's.

The persistent test server closed each connection twice, once from stop and
once from its serving thread, and the second close hit whatever socket had
reused that fd in the next test. stop now shuts connections down and only the
serving thread closes them.
Each test stood up its own server on port 18110. stop closes the listener, but on Linux a thread parked in accept() keeps it listening, so every test after the first failed with bind() failed on CI. One server for the namespace, through a :once fixture.
Tracking nightly surfaces a jolt change that breaks this library the day it lands. The floor in deps.edn stays at 0.8.16, but CI no longer tests it.
with-redefs is process-wide, and the in-process TLS server closes and frees
its end of the same connection on its own thread. On Linux that landed inside
the counting window and read as a second close, failing
closing-a-tls-stream-twice-releases-once on the nightly leg.
@yogthos
yogthos merged commit 7a9e4ec into main Oct 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant