The HTTP vocabulary, a sans-IO HTTP/1.1 codec and runtime-independent client and server connection engines for MoonBit. Depends on moonbitlang/core only; runs on every backend.
Status: this version is being rebuilt in the mttp repository. The packages arrive one at a time: the vocabulary, the runtime ports, their in-memory doubles, the HTTP/1.1 codec, the client and server connection engines and the test driver are here.
moon add gaato/http///|
test "describe a request" {
let request = @http.Request::post("https://api.example.com/v1/items")
.query("dry_run", "true")
.bearer("token")
.body_json({ "name": "first" })
assert_eq(request.url, "https://api.example.com/v1/items?dry_run=true")
assert_eq(request.headers.get("content-type"), Some("application/json"))
assert_false(@http.is_idempotent(request))
let url = @http.Url::parse(request.url)
assert_eq(url.host, Some("api.example.com"))
assert_eq(url.query_params(), [("dry_run", "true")])
}///|
/// Answers every request in memory, as a test double would.
struct Canned {
status : Int32
}
///|
impl @http.Backend for Canned with fn send(self, request, options) {
request.validate()
options.validate()
@http.Response::new(self.status, body=Bytes(b"ok"))
}
///|
test "send through a backend" {
let backend : &@http.Backend = Canned::{ status: 200, }
// `send` is async; a plain test drives it on the experimental driver.
let response = @driver.run_sync(() => {
backend.send(@http.Request::get("/ping"), @http.SendOptions::default())
})
assert_eq(response.status, 200)
backend.close()
}| Package | Status | What it is |
|---|---|---|
| gaato/http | stable | The vocabulary above, Backend and SendOptions, URL, form and HTTP-date helpers |
| gaato/http/h1 | stable | The sans-IO HTTP/1.1 codec: bounded head and chunked decoders, strict head parsers returning ParsedRequest and ParsedResponse, body framing that refuses request smuggling, and wire writers |
| gaato/http/rt | stable | The ports a runtime provides: Clock, Timer, Gate, Conn, Dialer, Signal, Coordination, and default_random |
| gaato/http/sim | stable | In-memory doubles of the ports on the driver (pair, ScriptedConn, FakeClock, ...), the shared contract checks every port implementation passes, and property-test generators. Test infrastructure |
| gaato/http/h1client | experimental | The HTTP/1.1 client connection engine over the rt ports: a per-origin connection pool, connection caps, bounded heads and bodies, per-phase deadlines and HTTP proxies (CONNECT tunnels, NO_PROXY), as an @http.Backend |
| gaato/http/h1server | experimental | The HTTP/1.1 server connection engine over the rt ports: serve_conn owns one accepted connection, with bounded heads, bodies, read-ahead, draining and phase deadlines |
| gaato/http/driver | experimental | A deterministic task driver with virtual time and run_sync, so async code is tested with plain test blocks on every target. It is built on compiler intrinsics and may break with a toolchain change |
pub(open) trait Backend {
async fn send(Self, Request, SendOptions) -> Response raise TransportError
fn close(Self) -> Unit = _
}pub(open) trait BodyStream {
async fn read_some(Self) -> Bytes? raise TransportError
fn close(Self) -> Unit
}pub struct AttributeKey[T] {
// private fields
}pub struct Attributes {
// private fields
}test {
let request = @http.Request::post("/upload").body_bytes(b"abc")
assert_eq(request.body, Bytes(b"abc"))
assert_eq(request.body.length(), Some(3L))
assert_true(request.body.is_replayable())
}test {
let options = { ..@http.SendOptions::default(), timeout_ms: Some(5000), }
assert_eq(options.body, Exchange(max_bytes=64 * 1024 * 1024))
let streaming = { ..options, body: Stream(idle_ms=Some(30000)), }
assert_eq(streaming.timeout_ms, Some(5000))
}test {
let headers = @http.Headers::of([("Accept", "text/plain")])
assert_eq(headers.get("ACCEPT"), Some("text/plain"))
}pub(all) struct Request {
meth : Method
url : String
headers : Headers
body : Body
attributes : Attributes
}fn Request::body_stream(self : Request, s : &BodyStream, length? : Int64, content_type? : String) -> Requesttest {
@http.SendOptions::default().validate()
let bad = { ..@http.SendOptions::default(), body: Exchange(max_bytes=-1), }
try bad.validate() catch {
@http.TransportError(kind~, phase~, ..) => {
assert_eq(kind, Invalid)
assert_eq(phase, Pool)
}
} noraise {
_ => fail("a negative byte limit must be refused")
}
}pub struct StreamBody {
// private fields
}impl Eq for StreamBodyimpl Debug for StreamBodypub(all) struct Trusted(String)pub struct UriBuilder {
// private fields
}test {
let base = @http.Url::parse("HTTPS://Example.COM:443/a/b?x=1")
assert_eq(base.to_string(), "https://example.com/a/b?x=1")
let next = base.resolve(@http.Url::parse("../c d"))
assert_eq(next.to_string(), "https://example.com/c%20d")
}test {
let url = @http.Url::parse("https://h/p?a=1#top")
let next = url.append_query_params([("b", "x y"), ("a", "2")])
assert_eq(next.to_string(), "https://h/p?a=1&b=x%20y&a=2#top")
}test {
let url = @http.Url::parse("https://h/p?page=1&q=a+b&page=2")
let next = url.merge_query_params([("page", "3")])
assert_eq(next.to_string(), "https://h/p?q=a+b&page=3")
}test {
let base = @http.Url::parse("http://a/b/c/d;p?q")
assert_eq(base.resolve(@http.Url::parse("../g")).to_string(), "http://a/b/g")
assert_eq(base.resolve(@http.Url::parse("//g")).to_string(), "http://g")
}fn append_query(url : String, name : String, value : String) -> Stringfn encode_utf8(text : String) -> Bytestest {
let lone = String::from_array([(0xD800).unsafe_to_char()])
assert_eq(@http.encode_utf8("é" + lone), b"\xC3\xA9\xEF\xBF\xBD")
}test {
assert_eq(
@http.form_urlencode([("q", "a b&c"), ("lang", "日本")]),
"q=a+b%26c&lang=%E6%97%A5%E6%9C%AC",
)
}fn is_success(status : Int) -> Boolfn join_url(base : String, target : String) -> Stringfn parse_http_date(s : String, now_unix_ms~ : Int64) -> Int64?test {
assert_eq(
@http.parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT", now_unix_ms=0L),
Some(784111777000L),
)
assert_eq(@http.parse_http_date("06 Nov 1994", now_unix_ms=0L), None)
}fn percent_encode(value : String) -> Stringfn percent_encode_path(value : String) -> Stringfn reason_phrase(status : Int) -> Stringtest {
let id = "a/b"
let q : String? = None
assert_eq(
@http.uri(b => b <+ "https://example.test/\{id}?q=\{q}&n=\{3}"),
"https://example.test/a%2Fb?n=3",
)
}Install
Download zipThe HTTP vocabulary, a sans-IO HTTP/1.1 codec and runtime-independent client and server connection engines for MoonBit. Depends on moonbitlang/core only; runs on every backend.