Module http/client
Async HTTP client — fetch and fetch_with, shaped like JavaScript's
fetch and configured like reqwest.
fetch(url, io) is a GET with defaults; fetch_with(url, opts, io) takes
a FetchOptions carrying the method, headers, body, redirect cap,
response ceiling and a deadline for the WHOLE exchange. Both speak
plaintext over TcpStream and TLS over TlsStream (https is real TLS,
not a stub), and both buffer the entire response into memory — which is
why max_response_bytes exists and defaults to 64 MiB rather than being
unlimited.
Each call is a fresh connection, and a Connection: close request header
says so on the wire: there is no pool yet (see ## Stability). Failures
throw IoExn carrying an HttpError (D1); a deadline that wins the race
throws HttpError.Timeout and aborts the in-flight task, so a hung server
cannot pin a caller forever.
{ fetch } :: import("std/http");
{ IoExn } :: import("std/error");
resp := io.await(fetch(`http://example.com/api/data`, io), IoExn(io : io, exn : exn));
println(resp.body);
Stability
unstable — the connection model is going to change. Every request sets
Connection: close and reads through read_http_message, the framing
form that DISCARDS whatever the peer sent past the message, so nothing
here can reuse a connection today; the reusable framing landed in
./wire.yo on 2026-09-10 and the pool is the remaining piece. Its
intended shape is decided — an HttpClient object that OWNS its idle
connections (Rust's reqwest::Client), with an explicit lifetime and a
Dispose that closes the idle set, rather than a module-global — and the
free fetch/fetch_with are meant to keep their one-shot behaviour when
it arrives, so this file grows a type rather than changing these two.
A second, smaller shape is open: a plaintext request connects to the
FIRST address lookup_host returns and does not fall back to the next on
failure, so a host whose IPv6 route is dead fails instead of reaching its
IPv4 address (no happy-eyeballs, and the resolver order decides). That
sits behind std/net/dns's own open questions.
Types
Options for configuring an HTTP request.
Fields
| Name | Type | Description |
|---|---|---|
method | HttpMethod | |
headers | HeaderMap | |
body | String | |
timeout | Option(Duration) | Deadline for the WHOLE request, redirects included — |
max_redirects | usize | How many 3xx |
max_response_bytes | usize | Ceiling on the raw response bytes (headers + body) buffered into
memory before |
impl(FetchOptions, ...)
new : (FetchOptions) fn() -> FetchOptionsCreate default options (GET, no headers, empty body, no timeout, 10 redirects, 64 MiB response ceiling).
Returns: FetchOptions
with_method : (FetchOptions) fn(self : FetchOptions, method : HttpMethod) -> FetchOptionsSet the HTTP method.
Parameters
| Name | Type | Notes |
|---|---|---|
self | FetchOptions | |
method | HttpMethod |
Returns: FetchOptions
with_header : (FetchOptions) fn(self : FetchOptions, name : String, value : String) -> FetchOptionsAdd a request header.
Parameters
| Name | Type | Notes |
|---|---|---|
self | FetchOptions | |
name | String | |
value | String |
Returns: FetchOptions
with_body : (FetchOptions) fn(self : FetchOptions, body : String) -> FetchOptionswith_timeout : (FetchOptions) fn(self : FetchOptions, limit : Duration) -> FetchOptionsGive the whole request (redirects included) a deadline.
Parameters
| Name | Type | Notes |
|---|---|---|
self | FetchOptions | |
limit | Duration |
Returns: FetchOptions
with_max_redirects : (FetchOptions) fn(self : FetchOptions, n : usize) -> FetchOptionsCap the number of redirect hops followed (0 = return 3xx verbatim).
Parameters
| Name | Type | Notes |
|---|---|---|
self | FetchOptions | |
n | usize |
Returns: FetchOptions
with_max_response_bytes : (FetchOptions) fn(self : FetchOptions, n : usize) -> FetchOptionsCap the raw response bytes buffered into memory (0 = unlimited).
Parameters
| Name | Type | Notes |
|---|---|---|
self | FetchOptions | |
n | usize |
Returns: FetchOptions
Functions
Perform an HTTP request with custom options: follows redirects up to
opts.max_redirects, refuses responses past opts.max_response_bytes,
and — when opts.timeout is set — races the whole exchange against the
deadline, throwing HttpError.Timeout if the deadline wins.
Example
opts := FetchOptions.new().with_method(.POST).with_body(`{"key": "value"}`).with_timeout(Duration.from_secs(i64(10)));
resp := io.await(fetch_with(`http://example.com/api`, opts, io), { io, exn });
Parameters
| Name | Type | Notes |
|---|---|---|
url_str | String | |
opts | FetchOptions | |
io | Io |
Returns: Impl(Future(HttpResponse, IoExn))
Perform an HTTP GET request to the given URL string.
Returns the HttpResponse on success.
Example
resp := io.await(fetch(`http://example.com`, io), { io, exn });
cond(resp.is_ok() => println(resp.body), true => println(`Error`));
Parameters
| Name | Type | Notes |
|---|---|---|
url_str | String | |
io | Io |
Returns: Impl(Future(HttpResponse, IoExn))
Constants
Default redirect cap — what browsers and curl use.
Value: 10
Default response ceiling: 64 MiB.
Value: 67108864