Abort :http streaming read when the consumer closes the channel#98
Merged
Conversation
The :http backend's streaming request had no way to abort an in-flight
socket read when the consumer closes `stream_to`. Closing the channel
stopped the chunk-reader task but left the read task blocked in
`eof(io)`/`readbytes!(io)` on the socket, so a streaming request whose
consumer stops early (e.g. a Kubernetes watch stopped via `close(stream)`
after the awaited event arrives, or a timer firing) would hang until the
read-idle timeout instead of returning promptly.
Add an abort-on-close watcher task, mirroring the :downloads backend:
capture the connection `io`, and when `stream_to` closes, close `io` and
schedule an `InterruptException` on the read task. `close(io)` alone does
not wake an HTTP/2 body read parked on the flow-control timer
(`_wait_h2_body_progress!` -> `timedwait`), so the scheduled interrupt is
what reliably unblocks it (the same fallback :downloads uses for
non-interruptible downloads). The read task swallows the abort (channel
closed, or our InterruptException) and returns normally; a genuine
network error or read-idle timeout arrives while `stream_to` is still
open, so it still propagates. The interrupt surfaces to callers as
`InvocationException("request was interrupted")` via `exec`, which
`is_request_interrupted` already recognizes.
Affects both HTTP.jl 1.x and 2.x (shared :http code path). Validated
against a live Kubernetes watch on HTTP 1.11 and 2.5: consumer-initiated
stop now returns in ~2-3s instead of hanging.
4d197b2 to
a89bb8a
Compare
krynju
approved these changes
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
:http(HTTP.jl) streaming backend cannot abort an in-flight socket read when the consumer closes the event channel (stream_to)._http_streaming_requestruns two tasks under@sync: one reads the socket into a buffer stream, the other turns chunks into objects andput!s them ontostream_to. Closingstream_toends the chunk-reader task, but nothing touches the connectionio— so the read task stays blocked ineof(io)/readbytes!(io)until the read-idle timeout.As a result, a streaming request whose consumer stops early hangs instead of returning. This is the normal control flow for a Kubernetes watch: the consumer reads events until the awaited one arrives (or a timer fires), then calls
close(stream)to stop — and then the whole call blocks up to the read-idle timeout (e.g. 10 min) rather than returning in a couple of seconds.The
:downloadsbackend does not have this problem: it already has a third watcher task that interrupts the download whenstream_tocloses. The:httpbackend was missing the equivalent.Fix
Add an abort-on-close watcher task to
_http_streaming_request, mirroring:downloads:iointo aRefwhen the request opens.stream_to; when it closes, itclose(io)andschedule(read_task, InterruptException()).InterruptException) and returns normally. A genuine network error or read-idle timeout arrives whilestream_tois still open, so it still propagates unchanged.Why the
InterruptExceptionand not justclose(io): on HTTP/2, closing the stream does not wake a body read parked on the flow-control timer (_wait_h2_body_progress!→timedwait); the read stays blocked. Scheduling anInterruptExceptionon the read task reliably unblocks it — the same fallback:downloadsuses for non-interruptible downloads.close(io)is kept as a best-effort first step (and is sufficient on HTTP/1.1).The interrupt surfaces to callers as
InvocationException("request was interrupted")viaexec(the existingresp === nothingpath), whichis_request_interruptedalready recognizes — so consumers that already catchis_request_interrupted/is_longpoll_timeoutneed no changes.Scope
Shared
:httpcode path — affects both HTTP.jl 1.x and 2.x. No public API change; non-streaming requests and normal streaming delivery are untouched.Validation
Reproduced the original hang and verified the fix against a live Kubernetes watch (Kuber.jl → OpenAPI) on both HTTP 1.11.0 and HTTP 2.5.1, Julia 1.12:
close(stream): returns in ~2–3s (previously hung to the read-idle timeout).All three previously hung. The genuine read-idle-timeout path (fires while
stream_tois still open) still throws as before, so existing reconnect/retry logic is preserved.Note
A patch release would be needed to consume this downstream (e.g. in Kuber.jl-based watch code). Left the version bump to maintainers.