Module http/client

http/client
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. — stable modules only change additively; this one may still change.

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

FetchOptions object
FetchOptions

Options for configuring an HTTP request.

Fields

NameTypeDescription
methodHttpMethod
headersHeaderMap
bodyString
timeoutOption(Duration)

Deadline for the WHOLE request, redirects included — .None waits forever. On expiry the in-flight connection is aborted and fetch throws HttpError.Timeout.

max_redirectsusize

How many 3xx Location hops to follow before throwing HttpError.TooManyRedirects. 0 returns the first 3xx verbatim.

max_response_bytesusize

Ceiling on the raw response bytes (headers + body) buffered into memory before HttpError.ResponseTooLarge is thrown; 0 = unlimited. This client buffers the whole response into a String, so the default (64 MiB) is what stands between an unbounded server and OOM.

impl(FetchOptions, ...)
new : (FetchOptions) fn() -> FetchOptions

Create 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) -> FetchOptions

Set the HTTP method.

Parameters

NameTypeNotes
selfFetchOptions
methodHttpMethod

Returns: FetchOptions

with_header : (FetchOptions) fn(self : FetchOptions, name : String, value : String) -> FetchOptions

Add a request header.

Parameters

NameTypeNotes
selfFetchOptions
nameString
valueString

Returns: FetchOptions

with_body : (FetchOptions) fn(self : FetchOptions, body : String) -> FetchOptions

Set the request body.

Parameters

NameTypeNotes
selfFetchOptions
bodyString

Returns: FetchOptions

with_timeout : (FetchOptions) fn(self : FetchOptions, limit : Duration) -> FetchOptions

Give the whole request (redirects included) a deadline.

Parameters

NameTypeNotes
selfFetchOptions
limitDuration

Returns: FetchOptions

with_max_redirects : (FetchOptions) fn(self : FetchOptions, n : usize) -> FetchOptions

Cap the number of redirect hops followed (0 = return 3xx verbatim).

Parameters

NameTypeNotes
selfFetchOptions
nusize

Returns: FetchOptions

with_max_response_bytes : (FetchOptions) fn(self : FetchOptions, n : usize) -> FetchOptions

Cap the raw response bytes buffered into memory (0 = unlimited).

Parameters

NameTypeNotes
selfFetchOptions
nusize

Returns: FetchOptions

Functions

fetch_with function
fn(url_str : String, opts : FetchOptions, io : Io) -> Impl(Future(HttpResponse, IoExn))

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

NameTypeNotes
url_strString
optsFetchOptions
ioIo

Returns: Impl(Future(HttpResponse, IoExn))

fetch function
fn(url_str : String, io : Io) -> 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

NameTypeNotes
url_strString
ioIo

Returns: Impl(Future(HttpResponse, IoExn))

Constants

DEFAULT_MAX_REDIRECTS constant usize

Default redirect cap — what browsers and curl use.

Value: 10

Default response ceiling: 64 MiB.

Value: 67108864