moonfetch

    An HTTP and WebSocket client for MoonBit, on native, wasm and the browser.

    http
    http3
    h3
    quic
    rfc9114
    rfc9000
    browser
    browser-fetch
    node
    javascript
    wasm
    rfc6265bis
    http-client
    client
    fetch
    requests
    httpx
    reqwest
    async
    http1
    http2
    https
    tls
    alpn
    redirect
    gzip
    deflate
    json
    form
    multipart
    stream
    streaming
    sse
    server-sent-events
    websocket
    ws
    wss
    cookie
    cookie-jar
    proxy
    connect
    socks5
    h2c
    http2-multiplexing
    rfc6265
    rfc6455
    rfc1928
    rfc7578
    rfc9110
    rfc9112
    rfc9113
    rfc7838
    alt-svc
    moonbit
    Download zip
    Version
    0.1.7
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    2K

    #moonfetch

    An HTTP and WebSocket client for MoonBit: one call for a request, a Client that keeps its connections, HTTP/1.1 and HTTP/2 chosen by ALPN, HTTP/3 over QUIC, streamed bodies, cookies, proxies — and the same code in a browser, where the host's fetch does the work.

    async fn fetch_a_page() -> Unit raise {
    let page = @moonfetch.get("https://example.com/")
    println("\{page.url}: \{page.status}, \{page.body.length()} octets")
    }

    moon add moonbitstack/moonfetch

    #What it does

    VersionsHTTP/1.1 and HTTP/2 by ALPN over TLS, HTTP/1.1 in the clear, HTTP/2 in the clear with h2c, and HTTP/3 over QUIC when asked for
    Hostsnative and wasm over moonnet's connections; the browser and Node over fetch
    Connectionsidle HTTP/1.1 connections kept per origin; one HTTP/2 or HTTP/3 connection per origin carries every request to it, many at once
    BodiesBody is one value: Text, Bytes, Json, Form, Multipart, Stream; the content type follows unless a header names one
    StreamingClient::stream returns as soon as the head is in; Stream::next hands over pieces as they arrive, for server-sent events and downloads
    Boundsa buffered body stops at Limits::body while it is being read, not after
    Redirectsup to 10 followed; a 303, and a 301 or 302 to a POST, become a GET; authorization and cookie stay with their origin
    Timeouts5 seconds for the whole exchange, redirects included; a stream bounds the head and each read instead
    Compressiongzip and deflate asked for and decoded; a request with its own accept-encoding gets the body as sent
    Cookiesa Jar follows RFC 6265bis: domain and path matching, expiry, the __Host- and __Secure- prefixes, the Public Suffix List
    ProxiesCONNECT for an HTTP proxy, SOCKS5 for a SOCKS one, with a username and password for either
    Retriesa kept connection the server closed while idle, or an HTTP/2 connection it went away from, is replaced and an idempotent request sent again; a stream the server refused is sent again whatever the method, and a request with a streamed body is never replayed
    Certificateschecked against the Mozilla roots by mooncred; there is no switch that turns checking off: trust your own CA by giving tls roots that hold it
    ErrorsRefused for what moonfetch refuses (Status, Redirects, Reset, Timeout, Url, Exceeded, Proxy); a socket, TLS or framing error passes through as it is

    #A client

    async fn calls(token : String) -> Unit raise {
    let api = @moonfetch.Client::new(
    base_url="https://api.example.com/v1",
    headers=[("authorization", "Bearer \{token}")],
    timeout=Some(10000),
    )
    defer api.close()
    let user = api.get("/users/7").checked()
    let made = api.post("/users", body=@moonfetch.Body::Json({ "name": "moon" }))
    let form = api.post("/login", body=@moonfetch.Body::Form([("user", "moon")]))
    println("\{user.status} \{made.status} \{form.status}")
    }

    base_url is the prefix relative URLs are joined to, as httpx joins them: its path is kept, so /users under https://a.example/api is https://a.example/api/users. headers go on every request; a headers given for one request replaces the client's of the same name.

    #Hosts

    The package has two transports and picks one at build time. Nothing in a program changes between them; what a host does not have is not offered, and the differences are these:

    native, wasmjs
    Transportmoonnet's sockets, TLS and ALPNthe host's fetch
    Client::newtls?, proxy?, jar?, http2~, h2c~, compression~, idle~credentials~
    Connections, proxies, cookiesthis library'sthe host's; close has nothing to close
    Response::versionthe version that answeredempty: a browser does not say
    Client::connecta connection to upgradenot there
    HTTP/3over QUIC by moonnetwhatever the host does itself

    A browser also decides the fields of RFC 9110 §10.1 that a page may not set, follows a cross-origin redirect itself, and rules on CORS before moonfetch sees anything.

    #HTTP/3

    http3=true speaks HTTP/3 over QUIC. It is offered for https only, since there is no HTTP/3 in the clear; an origin that does not answer on the datagram within three seconds is spoken to over TCP from then on, and the rest of the program does not see the attempt. A browser host has no such switch.

    async fn over_quic() -> Unit raise {
    let client = @moonfetch.Client::new(http3=true, timeout=Some(10000))
    defer client.close()
    let response = client.get("https://cloudflare.com/cdn-cgi/trace")
    println(response.version)
    }

    One QUIC connection carries every exchange to an origin, one bidirectional stream each, so requests overlap the way they do on HTTP/2. Settings advertise a QPACK table of no entries, which keeps a reply to the static table; the connection is moonnet's, which runs moonquic's state machine on the datagram socket.

    #Streaming

    stream sends the request, follows redirects, and returns once the response head is in. Each next gives a Data piece or the End with its trailers; the pieces are the octets as they came, not a line at a time.

    async fn feed(client : @moonfetch.Client) -> Unit raise {
    let stream = client.stream(@method.Method::Get, "/events")
    for ;; {
    match stream.next() {
    Some(@moonfetch.Data(bytes)) => println(bytes.length())
    Some(@moonfetch.End(trailers)) => {
    println(trailers.length())
    break
    }
    None => break
    }
    }
    }

    A stream is not bounded — that is what makes it a stream — so a caller that reads one to the end passes its own bound: stream.bytes(limit=1048576). A buffered request uses Limits::body and stops as the octets pass it.

    Streaming does not ask for compression: a coding would have to be undone on the fly. Ask for it yourself if you mean to.

    #Request bodies

    Text, Bytes, Json and Form are whole bodies. Multipart is a form with files, and Stream is a body produced while the request is sent:

    async fn upload(client : @moonfetch.Client, notes : Bytes) -> Unit raise {
    let part = @moonfetch.Part::file("doc", "notes.txt", notes, content_type="text/plain")
    ignore(client.post("/upload", body=@moonfetch.Body::Multipart([
    @moonfetch.Part::field("owner", "moon"),
    part,
    ])))
    }

    async fn put_kibibytes(client : @moonfetch.Client) -> Unit raise {
    let mut sent = 0
    let source = @moonfetch.Source::sized(4096, () => {
    guard sent < 4096 else { return None }
    sent = sent + 1024
    Some(Bytes::make(1024, b'x'))
    })
    ignore(client.put("/blob", body=@moonfetch.Body::Stream(source)))
    }

    A Source with a length declares content-length; without one the body is chunked on HTTP/1.1 and unframed on HTTP/2 and HTTP/3.

    #Seeing the bytes

    wire and frames write a request the way the transports do, without sending it: for a log, a test, or a page that wants to show a reader what it builds.

    let sent = @moonfetch.wire(@method.Method::Get, "https://example.com/a?b=1")
    assert_eq(sent.version, "HTTP/1.1")
    assert_eq(@utf8.decode_lossy(sent.pieces[0].1[:]), "GET /a?b=1 HTTP/1.1\r\nhost: example.com\r\nuser-agent: moonfetch\r\naccept: */*\r\n\r\n")

    frames gives the same request as HTTP/2 writes it: the connection preface, this side's settings, and one entry per frame, each labelled with its type and flags — HEADERS + END_STREAM, DATA, DATA + END_STREAM. Both take the same Body a request does, so the octets are the ones that would go out, not a sketch of them.

    #Cookies

    async fn browser() -> Unit raise {
    let jar = @moonfetch.Jar::new()
    let client = @moonfetch.Client::new(jar~)
    defer client.close()
    ignore(client.get("https://example.com/login"))
    ignore(client.get("https://example.com/account"))
    }

    One jar may be shared by several clients and by tasks; it lives as long as the process, since persistence is not part of it.

    #Proxies

    fn through_a_proxy() -> Unit raise {
    let http = @moonfetch.Proxy::parse("http://user:pass@proxy.internal:3128")
    let socks = @moonfetch.Proxy::parse("socks5://proxy.internal:1080")
    println("\{http.host} \{socks.host}")
    }

    An http request through an HTTP proxy goes in absolute form; an https one is tunnelled with CONNECT. SOCKS5 sends the host as a name and lets the proxy resolve it.

    #WebSocket

    moonfetch/ws shares the client's TLS roots, proxy and timeouts:

    async fn chat(client : @moonfetch.Client) -> Unit raise {
    let socket = @ws.connect(client, "wss://example.com/chat", protocols=["chat"])
    socket.send_text("hello")
    while socket.recv() is Some(message) {
    match message {
    @ws.Text(text) => println(text)
    @ws.Binary(bytes) => println(bytes.length())
    @ws.Close(status~, ..) => {
    println(status)
    break
    }
    }
    }
    socket.close(status=1000)
    }

    Pings are answered and fragmented messages put back together inside recv, so a caller sees messages, not frames.

    #Consistency

    What this client was checked against, and what it came out as.

    SuiteWhereResult
    This repository's cases, over HTTP/1.1, HTTP/2 and HTTP/3every push, three systemspass
    The same cases on the js host, against a server written on Node's httpevery pushpass
    HTTP/3 against nginx 1.29 (--with-http_v3_module, in a container) and against mooncat's QUIC listener, real traffic eachevery pushpass
    21 public HTTPS origins, 15 of them over HTTP/2once, by hand19 of 21; the two that fail offer TLS 1.2 only, which moontls does not speak

    #Beside the official client

    moonbitlang/async/http has a client too. What differs:

    async/httpmoonfetch
    HTTP/2noby ALPN, many streams on one connection
    HTTP/3noover QUIC, with a fallback to TCP
    Streamingwhole bodiesStream, a piece at a time
    Cookies, proxiesmanualJar, CONNECT and SOCKS5
    WebSocketa separate clientmoonfetch/ws, on the same client
    TLSthe system's (OpenSSL, SChannel)moontls, the same on every platform
    Connection reuseone connection per Clienta pool per origin
    Redirects, compressionnoyes
    Browser hostfetch behind the same interfacefetch behind the same interface
    Turning certificate checks offtrust=NoVerificationnot possible; give the roots instead

    #Licence

    Apache-2.0. See LICENSE.

    Refused

    pub(all) suberror Refused {
    Status(status~ : Int, url~ : String)
    Redirects(limit~ : Int)
    Reset(code~ : Int)
    Timeout(limit~ : Int)
    Url(String)
    Exceeded(limit~ : Int, got~ : Int)
    Proxy(String)
    } derive(Eq,
    Debug
    )

    What moonfetch itself refuses. Errors from below pass through as they are: a socket that will not connect, a TLS alert, a response that does not parse.
    impl Show for Refused

    Refused::equal

    fn Refused::equal(Refused, Refused) -> Bool

    Refused::not_equal

    fn Refused::not_equal(x : Refused, y : Refused) -> Bool

    Refused::output

    fn Refused::output(self : Refused, logger : &Logger) -> Unit

    Refused::to_repr

    Refused::to_string

    fn Refused::to_string(self : Refused) -> String

    Body

    pub(all) enum Body {
    Empty
    Text(String)
    Bytes(Bytes)
    Json(
    Json
    )
    Form(Array[(String, String)])
    Multipart(Array[Part])
    Stream(Source)
    } derive(Eq)

    A request body. One value, so a request cannot carry two bodies for the library to choose between.

    Body::equal

    fn Body::equal(Body, Body) -> Bool

    Body::not_equal

    fn Body::not_equal(x : Body, y : Body) -> Bool

    Body::to_repr

    Chunk

    pub(all) enum Chunk {
    Data(Bytes)
    End(Array[(String, String)])
    }

    A piece of a response body as it arrives.

    Client

    type Client

    An HTTP client that keeps its connections: idle HTTP/1.1 ones per origin, and one HTTP/2 connection per origin for every request to it. Close it when done with it.

    Client::close

    fn Client::close(self : Client) -> Unit

    Close every connection the client keeps.

    Client::connect

    async fn Client::connect(self : Client, url : StringView) ->
    Conn

    A fresh connection to the URL's origin, with the client's TLS and proxy rules applied and nothing else on it. The caller owns it; it is not taken from the pool and is not kept. For a protocol that starts as HTTP and then becomes something else — WebSocket, say.

    Client::delete

    async fn Client::delete(self : Client, url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    Client::get

    async fn Client::get(self : Client, url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    Client::head

    async fn Client::head(self : Client, url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    Client::new

    fn Client::new(base_url? : String, headers? : Array[(String, String)], timeout? : Int?, redirects? : Int, http2? : Bool, compression? : Bool, http3? : Bool, idle? : Int, h2c? : Bool, limits? : Limits, jar? : Jar, proxy? : Proxy, tls? :
    Client
    ) -> Client raise Refused

    A client.

    base_url is the prefix relative URLs are joined to, as httpx joins them: its path is kept, so /users under https://a.example/api is https://a.example/api/users. headers go on every request. timeout bounds each request, redirects included; None waits for ever. redirects caps how many are followed; 0 follows none. http2 offers HTTP/2 by ALPN; h2c speaks HTTP/2 with prior knowledge over http, where there is no ALPN to ask. compression asks for gzip or deflate and decodes what comes back, as httpx and reqwest do; a request that names its own accept-encoding gets the body as sent. http3 speaks HTTP/3 over QUIC where it can, and falls back to TCP for an origin that does not answer on the datagram, as a browser's Alt-Svc does without the cache. limits bounds what a buffered response may hold. jar keeps cookies across requests. proxy sends the connection through an HTTP or SOCKS5 proxy. tls judges servers' certificates, the Mozilla roots when not given; its ALPN list is set by http2.

    Client::options

    async fn Client::options(self : Client, url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    Client::patch

    async fn Client::patch(self : Client, url : StringView, body? : Body, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    Client::post

    async fn Client::post(self : Client, url : StringView, body? : Body, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    Client::put

    async fn Client::put(self : Client, url : StringView, body? : Body, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    Client::request

    async fn Client::request(self : Client, verb :
    Method
    , url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], body? : Body, timeout? : Int, redirects? : Int) -> Response

    Send a request and read the response whole.

    query is added to the URL's own query; headers replace the client's of the same name; timeout and redirects replace the client's for this request.

    Client::stream

    async fn Client::stream(self : Client, verb :
    Method
    , url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], body? : Body, timeout? : Int, redirects? : Int) -> Stream

    Send a request and return once its head is in; the body is read with Stream::next.

    Redirects are followed as request follows them, the body of each hop read and dropped first. timeout bounds reading the head and each later Stream::next, not the whole exchange, which is the point of streaming.

    Jar

    pub struct Jar {
    // private fields
    }

    A cookie store for a Client (RFC 6265bis).

    One jar may be shared by several clients and by tasks: every change goes through a lock. Persistence is not part of it — a jar lives as long as the process does.

    Jar::clear

    fn Jar::clear(self : Jar) -> Unit

    Jar::cookies

    async fn Jar::cookies(self : Jar, url :
    Url
    ) -> String

    The Cookie header for a request to url: the cookies that match, longest path first and oldest first within a path (RFC 6265bis §5.4).

    Jar::new

    fn Jar::new() -> Jar

    An empty jar.

    Jar::size

    fn Jar::size(self : Jar) -> Int

    How many cookies the jar holds.

    Jar::store

    async fn Jar::store(self : Jar, url :
    Url
    , values : Array[String]) -> Unit

    Take the cookies out of a response (RFC 6265bis §5.3).

    Limits

    pub(all) struct Limits {
    head : Int
    fields : Int
    body : Int
    }

    What a client reads of a response before it refuses it.

    head bounds the status line and header block, fields the number of header fields, and body the body as it arrives, before any content coding is undone. The body bound is checked as the octets come in, so a server that would send more is stopped early rather than after the allocation.

    Limits::new

    fn Limits::new(head? : Int, fields? : Int, body? : Int) -> Limits

    The same bounds with some replaced.

    Part

    pub struct Part {
    name : String
    content : Bytes
    filename : String?
    content_type : String?
    } derive(Eq)

    One part of a multipart/form-data body (RFC 7578): a form field when filename is None, a file otherwise.

    Part::content

    fn Part::content(self : Part) -> Bytes

    Part::content_type

    fn Part::content_type(self : Part) -> String?

    Part::equal

    fn Part::equal(Part, Part) -> Bool

    Part::field

    fn Part::field(name : StringView, text : StringView) -> Part

    A form field.

    Part::file

    fn Part::file(name : StringView, filename : StringView, content : Bytes, content_type? : StringView) -> Part

    A file part. content_type defaults to application/octet-stream, as browsers do.

    Part::filename

    fn Part::filename(self : Part) -> String?

    Part::name

    fn Part::name(self : Part) -> String

    Part::not_equal

    fn Part::not_equal(x : Part, y : Part) -> Bool

    Proxy

    pub(all) struct Proxy {
    scheme : String
    host : String
    port : Int
    user : String?
    password : String?
    }

    A proxy a client sends its connections through.

    http and https are the CONNECT method of RFC 9110 §9.3.6 — an https proxy speaks TLS to the proxy itself — and socks5 is RFC 1928, with the host sent as a name so the proxy resolves it. The user and password, when present, authenticate to the proxy: Basic for CONNECT, username and password for SOCKS5.

    Proxy::parse

    fn Proxy::parse(text : StringView) -> Proxy raise Refused

    A proxy from a URL: http://user:pass@host:1080, socks5://host:1080.

    Response

    pub(all) struct Response {
    status : Int
    reason : String
    version : String
    headers : Array[(String, String)]
    url : String
    body : Bytes
    } derive(Eq,
    Debug
    )

    A response, read whole. Header names are lower case, as HTTP/2 writes them, and keep the order they arrived in.

    Response::checked

    fn Response::checked(self : Response) -> Response raise Refused

    The response itself, or Status for a 4xx or 5xx, as reqwest's error_for_status does.

    Response::equal

    fn Response::equal(Response, Response) -> Bool

    Response::header

    fn Response::header(self : Response, name : StringView) -> String?

    The first value of the header name, matched without regard to case.

    Response::is_success

    fn Response::is_success(self : Response) -> Bool

    Whether the status is 2xx.

    Response::json

    The body parsed as JSON.

    Response::not_equal

    fn Response::not_equal(x : Response, y : Response) -> Bool

    Response::text

    fn Response::text(self : Response) -> String

    The body as UTF-8 text; a malformed sequence reads as U+FFFD.

    Response::to_repr

    Response::values

    fn Response::values(self : Response, name : StringView) -> Array[String]

    Every value of the header name, in order: what set-cookie needs.

    Source

    pub struct Source {
    length : Int64?
    step : async () -> Bytes?
    }

    A request body that is not known when the request is made: the steps are called as the connection has room for more.
    impl Eq for Source

    Source::equal

    fn Source::equal(self : Source, other : Source) -> Bool

    Source::length

    fn Source::length(self : Source) -> Int64?

    The length the source promises, when it promises one.

    Source::new

    fn Source::new(step : async () -> Bytes?) -> Source

    A body whose length is not known in advance; HTTP/1.1 sends it chunked, HTTP/2 as data frames.

    Source::next

    async fn Source::next(self : Source) -> Bytes?

    The next piece, or None at the end.

    Source::not_equal

    fn Source::not_equal(self : Source, other : Source) -> Bool

    Source::sized

    fn Source::sized(length : Int64, step : async () -> Bytes?) -> Source

    A body of exactly length octets: HTTP/1.1 and HTTP/2 both declare the length, so no chunking and no close-delimited body. A source that ends early or runs long fails the request.

    Source::to_repr

    Stream

    pub struct Stream {
    // private fields
    }

    A response whose body is read a piece at a time, for a body too large to hold or one that never ends: a server-sent event feed, a download that is written as it comes.

    One task reads one stream. close gives up the rest; a program that just drops it leaves the exchange unfinished until the client is closed.

    Stream::bytes

    async fn Stream::bytes(self : Stream, limit? : Int) -> Bytes

    Read the rest of the body, refusing to hold more than limit octets. The stream is over afterwards.

    Stream::close

    fn Stream::close(self : Stream) -> Unit

    Give up the rest of the body: an HTTP/2 stream is reset, an HTTP/1.1 connection is closed.

    Stream::header

    fn Stream::header(self : Stream, name : StringView) -> String?

    The first value of the header name, matched without regard to case.

    Stream::headers

    fn Stream::headers(self : Stream) -> Array[(String, String)]

    Stream::next

    async fn Stream::next(self : Stream) -> Chunk?

    Read the next piece, None once the body has been read to its end.

    Stream::reason

    fn Stream::reason(self : Stream) -> String

    Stream::status

    fn Stream::status(self : Stream) -> Int

    Stream::text

    async fn Stream::text(self : Stream, limit? : Int) -> String

    The rest of the body as UTF-8 text; a malformed sequence reads as U+FFFD.

    Stream::url

    fn Stream::url(self : Stream) -> String

    Stream::version

    fn Stream::version(self : Stream) -> String

    Wire

    pub(all) struct Wire {
    version : String
    pieces : Array[(String, Bytes)]
    }

    How a request would go on the wire: the version that would carry it, and the pieces it is written as, in order.

    The pieces of an HTTP/1.1 request are its head and then its body; those of an HTTP/2 one are the connection preface, this side's settings, and then one entry per frame, labelled with the frame's type and the flags it carries.

    delete

    async fn delete(url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    frames

    fn frames(verb :
    Method
    , url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], body? : Body) -> Wire raise

    The frames an HTTP/2 request is written as: the connection preface, this side's settings, and the HEADERS and DATA frames of the exchange.

    The connection is a fresh one, so the field section is encoded against the static table alone, which is what a first request on a connection looks like.

    get

    async fn get(url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    async fn head(url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    idle

    let idle : Int

    How many idle HTTP/1.1 connections are kept per origin: urllib3's 10.

    limits

    let limits : Limits

    The bounds a client uses when none are given: a 64 KiB head, 256 fields, and a 32 MiB body.

    No client of the common ones bounds a body by default; a library that returns a Response with the whole body in memory has to, or one answer can exhaust the process. 32 MiB covers the API payloads a buffered call is for; anything larger is read as a Stream, which is not bounded.

    options

    async fn options(url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    patch

    async fn patch(url : StringView, body? : Body, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    post

    async fn post(url : StringView, body? : Body, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    put

    async fn put(url : StringView, body? : Body, query? : Array[(String, String)], headers? : Array[(String, String)], timeout? : Int, redirects? : Int) -> Response

    redirects

    let redirects : Int

    How many redirects are followed when nothing else is said: reqwest's and requests' 10.

    A host that hands the redirect to its own stack follows as many as it allows; this is the number requests say.

    request

    async fn request(verb :
    Method
    , url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], body? : Body, timeout? : Int, redirects? : Int) -> Response

    One request on a client of its own, closed afterwards. A program that makes more than a few requests keeps a Client instead.

    timeout

    let timeout : Int

    How long a request may take when nothing else is said, in milliseconds: httpx's 5 seconds.

    wire

    fn wire(verb :
    Method
    , url : StringView, query? : Array[(String, String)], headers? : Array[(String, String)], body? : Body) -> Wire raise

    The octets an HTTP/1.1 request is written as, for a caller that wants to see them: the head with its framing, then the body.

    A body produced while the request goes out is not read here — taking its pieces would spend them — so only the head is written, with the framing that says a body follows.

    Refused::Url for a URL no request can name, and the state machine's own refusal for a field it cannot write.