Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,15 @@ These host behaviors may affect application design:
leave the `Request` at `NoTimeout`. After an idle timeout the response can
still be read; the `Request` deadline passing is final. Each streaming
response has its own connection, closed when the body ends or fails, or when
the response and its streams are released. Request bodies are not streamed.
the response and its streams (and, for `Http.start!`, its pending request)
are released. Request bodies are not streamed.
- `Http.start!` keeps a request in flight while the app does other work,
including other requests; `pending.wait_within!(duration)` waits for its
response for up to `duration`. There is no background thread: requests make
progress only during HTTP calls and waits. Both timeouts count from
`start!`, so a slow server needs a larger `idle_timeout`; a response that
arrived in time is not lost to a later wait. Releasing the last reference to
the pending request, or `pending.close!()`, cancels it.
- HTTP redirects are opt-in through trailing options, `{ max_redirects: n }`;
the default is zero. The request deadline covers every hop and the final
body. Cross-origin hops strip credentials; HTTPS downgrades are refused.
Expand Down
11 changes: 11 additions & 0 deletions ci/rust_http_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ async fn handle_request(req: Request<Incoming>) -> Result<Response<BoxBody>, Gen
if req.uri().path().starts_with("/redirect/") {
return handle_redirect(req).await;
}
// Answers only after the given number of milliseconds, as a server does
// with a long-running request. Used by tests/http-in-flight.roc and
// examples/http-pending.roc.
if let Some(ms) = req.uri().path().strip_prefix("/delay/") {
let ms: u64 = ms.parse()?;
tokio::time::sleep(Duration::from_millis(ms)).await;
return Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain; charset=utf-8")
.body(full(format!("waited {ms} ms")))?);
}
let response = match (req.method(), req.uri().path()) {
(&Method::GET, "/utf8test") => {
// UTF-8 encoded "Hello utf8"
Expand Down
64 changes: 64 additions & 0 deletions examples/http-pending.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
## Keep a slow request in flight while other requests report on its progress.
app [main!] {
http: "https://github.com/roc-lang/http/releases/download/1.0.0/6ZUwqYhCS8PU9Mo6MF7oV82ET2o7KYb57CLKDq4cq4sS.tar.zst",
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.27.0/9yjxqdGTP4xSpSEmDazwTmRsehfRZzA19pwEKkR6fj1g.tar.zst",
}

import pf.Duration
import pf.Http
import pf.OsStr
import pf.Stdout
import http.Request

# To run this example, first start the test server in another terminal:
#
# cd ci/rust_http_server && cargo run --release
#
# then:
#
# roc build examples/http-pending.roc
# ./examples/http-pending
main! : List(OsStr) => Try({}, _)
main! = |_args| {
# The server answers only after two seconds, as a server does with a long
# commit. That is longer than a response normally takes, so the request
# must not keep the default 30 second `idle_timeout`, which would end the
# wait for a commit that takes minutes. `NoTimeout` leaves it to the
# `Request` timeout, which is off by default.
commit_request = Request.from_method(POST).with_uri("http://127.0.0.1:9000/delay/2000")

# `start!` sends the request and returns at once, with a handle to the
# response that is still to come.
commit = Http.start!(commit_request, { idle_timeout: NoTimeout }) ? |err| CommitFailed(err)

# Each `wait_within!` waits up to half a second for the commit. While it
# waits it also moves the commit along, so the loop needs no `Sleep`. When
# the commit has not finished by then, ask for its progress and wait again.
var $progress_checks = 0.U64
while True {
match commit.wait_within!(Duration.millis(500)) {
Ok(Ready(response)) => {
# The commit's own response: status and headers are here, the
# body is read as with `Http.send_streaming!`.
body = response.read_to_end!(NoLimit) ? |err| CommitFailed(err)
Stdout.line!("Checked progress while the commit ran: ${Str.inspect($progress_checks >= 2)}")?
Stdout.line!("Commit finished with status ${response.status().to_str()}: ${Str.from_utf8_lossy(body)}")?
break
}
Ok(NotReady) => {
# Any other request can be sent now. The commit stays in flight.
progress = Http.get_utf8!("http://127.0.0.1:9000/utf8test", {}) ? |err| ProgressFailed(err)
$progress_checks = $progress_checks + 1
Stdout.line!("Progress ${$progress_checks.to_str()}: ${progress}")?
}
Err(err) => {
return Err(CommitFailed(err))
}
}
}

# To give up on a commit that is still running, call `commit.close!()`: the
# request is cancelled and its connection closed. To wait for it without
# checking on anything else, call `commit.wait!()`.
Ok({})
}
15 changes: 15 additions & 0 deletions platform/Host.roc
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,19 @@ Host :: [].{
unix_socket_listen! : NativePath => Try(Resource.UnixSocketListener, [UnixSocketErr(IOErr)])
unix_socket_accept! : Resource.UnixSocketListener, [NoTimeout, After(U64)] => Try(Resource.UnixSocketConnection, [Closed, UnixSocketErr(IOErr)])
unix_socket_listener_close! : Resource.UnixSocketListener => Try({}, [UnixSocketErr(IOErr)])

# `http_start!` sends a request from a task on the HTTP runtime and returns
# at once; the task runs only while a hosted HTTP call or wait is inside the
# runtime. Every failure of the request, including one to build it, is
# reported by `http_pending_wait!`; the error here means that no handle
# could be made. `http_pending_wait!` gives up at the `At` instant (the
# `monotonic_now!` clock) with `NotReady`, which keeps the request. Once the
# head has arrived, every wait returns it with the same body handle, and
# with the status and `Location` of each redirect followed so far, in
# order. `http_pending_follow!` replaces the request behind a handle by the
# next redirect hop and records the status and `Location` that led there.
http_start! : InternalHttp.RequestToAndFromHost, [NoTimeout, After(U64)], [NoTimeout, At(U64)] => Try(Resource.HttpPending, List(U8))
http_pending_wait! : Resource.HttpPending, [NoTimeout, At(U64)] => Try([Ready({ response : InternalHttp.StreamingResponseFromHost, redirects : List((U16, Str)) }), NotReady], [Closed, HttpErr(InternalHttp.TransportErr)])
http_pending_follow! : Resource.HttpPending, InternalHttp.RequestToAndFromHost, U16, Str => {}
http_pending_close! : Resource.HttpPending => {}
}
Loading
Loading