An HTTP and WebSocket client for MoonBit, on native, wasm and the browser.
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| Versions | HTTP/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 |
| Hosts | native and wasm over moonnet's connections; the browser and Node over fetch |
| Connections | idle HTTP/1.1 connections kept per origin; one HTTP/2 or HTTP/3 connection per origin carries every request to it, many at once |
| Bodies | Body is one value: Text, Bytes, Json, Form, Multipart, Stream; the content type follows unless a header names one |
| Streaming | Client::stream returns as soon as the head is in; Stream::next hands over pieces as they arrive, for server-sent events and downloads |
| Bounds | a buffered body stops at Limits::body while it is being read, not after |
| Redirects | up to 10 followed; a 303, and a 301 or 302 to a POST, become a GET; authorization and cookie stay with their origin |
| Timeouts | 5 seconds for the whole exchange, redirects included; a stream bounds the head and each read instead |
| Compression | gzip and deflate asked for and decoded; a request with its own accept-encoding gets the body as sent |
| Cookies | a Jar follows RFC 6265bis: domain and path matching, expiry, the __Host- and __Secure- prefixes, the Public Suffix List |
| Proxies | CONNECT for an HTTP proxy, SOCKS5 for a SOCKS one, with a username and password for either |
| Retries | a 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 |
| Certificates | checked 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 |
| Errors | Refused for what moonfetch refuses (Status, Redirects, Reset, Timeout, Url, Exceeded, Proxy); a socket, TLS or framing error passes through as it is |
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}")
}| native, wasm | js | |
|---|---|---|
| Transport | moonnet's sockets, TLS and ALPN | the host's fetch |
| Client::new | tls?, proxy?, jar?, http2~, h2c~, compression~, idle~ | credentials~ |
| Connections, proxies, cookies | this library's | the host's; close has nothing to close |
| Response::version | the version that answered | empty: a browser does not say |
| Client::connect | a connection to upgrade | not there |
| HTTP/3 | over QUIC by moonnet | whatever the host does itself |
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)
}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
}
}
}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)))
}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")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"))
}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}")
}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)
}| Suite | Where | Result |
|---|---|---|
| This repository's cases, over HTTP/1.1, HTTP/2 and HTTP/3 | every push, three systems | pass |
| The same cases on the js host, against a server written on Node's http | every push | pass |
| HTTP/3 against nginx 1.29 (--with-http_v3_module, in a container) and against mooncat's QUIC listener, real traffic each | every push | pass |
| 21 public HTTPS origins, 15 of them over HTTP/2 | once, by hand | 19 of 21; the two that fail offer TLS 1.2 only, which moontls does not speak |
| async/http | moonfetch | |
|---|---|---|
| HTTP/2 | no | by ALPN, many streams on one connection |
| HTTP/3 | no | over QUIC, with a fallback to TCP |
| Streaming | whole bodies | Stream, a piece at a time |
| Cookies, proxies | manual | Jar, CONNECT and SOCKS5 |
| WebSocket | a separate client | moonfetch/ws, on the same client |
| TLS | the system's (OpenSSL, SChannel) | moontls, the same on every platform |
| Connection reuse | one connection per Client | a pool per origin |
| Redirects, compression | no | yes |
| Browser host | fetch behind the same interface | fetch behind the same interface |
| Turning certificate checks off | trust=NoVerification | not possible; give the roots instead |
type Clientpub struct Jar {
// private fields
}pub(all) struct Limits {
head : Int
fields : Int
body : Int
}pub struct Part {
name : String
content : Bytes
filename : String?
content_type : String?
} derive(Eq)pub(all) struct Proxy {
scheme : String
host : String
port : Int
user : String?
password : String?
}pub struct Source {
length : Int64?
step : async () -> Bytes?
}pub struct Stream {
// private fields
}let redirects : Intlet timeout : IntInstall
Download zipAn HTTP and WebSocket client for MoonBit, on native, wasm and the browser.