Repository navigation
Add Http.start! to keep an HTTP request in flight while the app does other work - #553
Draft
Karakatiza666 wants to merge 4 commits into
Draft
Karakatiza666 wants to merge 4 commits into
Karakatiza666 wants to merge 4 commits into
Conversation
Move the streaming client, the conversion of a response head to and from the host ABI, the timeout arguments and the redirect decision into helpers of their own, so that another way of sending a request can reuse them. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`http_start!` sends a request from a task on the HTTP runtime and returns a `Resource.HttpPending` handle at once. `http_pending_wait!` waits up to a given instant for the status and headers and leaves the request in flight when they have not arrived; once they have, every wait returns the same head and body handle. The task notes when the head arrived, and the idle timeout and deadline are judged by that time, so a head that came in time during another HTTP call is still returned by a later wait. `http_pending_follow!` sends the next redirect hop behind the same handle and records it; each wait that returns a head also returns the redirects followed so far, so that the redirect policy stays in Roc. Closing or releasing the handle aborts the request and closes its connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Http.start!` sends a request and returns an `Http.PendingResponse` without waiting for it. `wait_within!` waits up to a given duration for the status and headers and returns `NotReady` without cancelling the request; `wait!` waits as `send_streaming!` would. Both return the same `StreamingResponse` and errors as `send_streaming!`, and follow redirects with the same rules and one deadline across every hop. `close!`, or releasing the last reference, cancels the request. There is no background thread, so a pending request makes progress only during HTTP calls and waits; the documentation of `start!` says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust tests in src/http.rs cover Http.start! on the host side: not ready and then ready, a zero wait, other HTTP calls while a request is in flight, interruption, drop and close ending the connection, a returned response outliving its pending request, the request timeout counting from the start, a head that arrived in time outliving the timeouts, the idle timeout, transport errors, one deadline across a redirect hop, and the same bytes on the wire as send_streaming. The test server gets a /delay/<ms> route. tests/http-in-flight.roc runs the Roc API against it, and examples/http-pending.roc shows the pattern of waiting on a long request while checking its progress with other requests. Both have entries in scripts/test_spec.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Closes #551
Motivation
Http.send!andHttp.send_streaming!block until the response headers arrive, and the only way to stop waiting is a timeout, which cancels the request. That makes this pattern impossible: a long "commit" request is sent, and while the server works on it the app queries a progress endpoint once per second and prints the result. This PR addsHttp.start!, which sends a request and returns a handle without waiting. The app can then wait on the handle with a time limit (running out of time does not cancel anything), send other requests, or cancel.Roc API
Usage (see
examples/http-pending.roc):Semantics
NotReadydoes not cancel the request. It stays in flight and can be waited on again.Err(Interrupted)(a caught signal) keeps the request too. Errors are otherwise the same assend_streaming!, and an expired timeout isErr(HttpErr(Timeout))on every later wait.PendingResponse, orclose!, cancels the request and closes its connection. A response already returned stays readable. Afterclose!, waits returnErr(Closed).send_streaming!: both use the sharedredirect_step!. One deadline covers all hops.wait_within!(Duration.zero)that sees a redirect only starts the next hop, so it returnsNotReadyuntil that hop's response has arrived.start!, not from the wait. The request timeout is the deadline for the whole exchange;idle_timeoutbounds each hop's wait for status and headers. A head that arrived in time (judged by arrival time recorded by the task) is returned by a later wait, however late. A slow server needs a largeridle_timeout, orNoTimeout, as in the example.start!fails only for an invalid URL or when the host cannot allocate a handle. Failures of the request itself come from the waits.Progress model
Option A, as in the issue: there is no background thread. A pending request is sent and answered only while the app is inside an HTTP call or a wait (
start!,send!,send_streaming!, a body read,wait_within!,wait!). In between, the kernel buffers what arrives. A large request body, or a server that sends a lot before its status line, can stall until the next such call.wait_within!also drives all other pending requests, so a loop of waits needs noSleep. This is documented onHttp.start!.Option B, a background thread driving the runtime, is a possible follow-up. It is not in this PR because it changes the runtime and shutdown rules documented in
src/http.rs, and risks the exit hang described there. The API above would not need to change for it, so it can follow separately.Host design
HttpPending(platform/Resource.roc,src/resources.rs).http_start!spawns a tokio task onTOKIO_RUNTIMEthat sends the request and notes when the head arrives.await_io, so signals and the other waits behave as before.platform/Host.roc:http_start!,http_pending_wait!,http_pending_follow!,http_pending_close!.http_pending_follow!replaces the request behind the handle with the next hop and records its (status,Location); eachReadypayload ofhttp_pending_wait!carries the list of followed redirects, and Roc replays it (current_hop) to recover the current request, URL and remaining count.PendingResponseis an immutable Roc value, so it cannot carry the current hop across aNotReady; the hop has to be remembered somewhere that outlives one call.NotReady(PendingResponse): older copies go stale, and every caller has to thread the new value through its loop.Urlhandling inredirect_step!, and the two paths could drift apart.Requestand convert it back: this needs a lossy inverse of the request conversion and Roc-owned memory inside a resource.Choices worth a look
StreamingResponse(asChild.wait!does), so the pending value keeps the body's connection alive while it exists. Copies share one body cursor.PendingResponseholds the originalRequest, body included, because 307/308 replay it.close!returns{}, unlikeChild.close!, since there is nothing to report: closing twice or after completion is a no-op.wait_within!, notpoll!:Child.poll!takes no duration and never waits, which is not what this does.Commits
redirect_step!) into helpers.Http.start!(Roc API, docs, README).Reviewing per commit is easiest; commit 1 can be skimmed for "same behaviour".
Testing
src/http.rs: not ready then ready, zero wait, other HTTP calls during a request, interruption, drop and close ending the connection, a returned response outliving its pending request, request timeout counting from start, a head that arrived in time outliving the timeouts, idle timeout, transport errors, one deadline across a redirect hop, and the same bytes on the wire assend_streaming!.ci/rust_http_servergets a/delay/<ms>route.tests/http-in-flight.rocruns the Roc API against it (named so its binary does not clash with the example's).examples/http-pending.rocshows the progress pattern. Both are inscripts/test_spec.json.cargo fmt, clippy,cargo testthree times, glue check,roc test, the fullscripts/test.py, and an x64musl build with a valgrind run.Not tested:
InsecureRedirectthrough a pending request: there is no HTTPS test server, and the check is in the sharedredirect_step!.give_up_athas passed are covered only probabilistically.Coordination
A separate PR on TLS trust options touches
src/http.rsandplatform/Http.roc. This PR does not changehttps_connector()or TLS setup. Small conflicts are possible instreaming_client()and the timeout lines ofhosted_http_send_streaming. If that PR adds per-request options toStreamingOptions,Http.start!andhttp_start!must pass them as well.🤖 Generated with Claude Code