http

    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.

    http
    sans-io
    http1
    codec
    Download zip
    Author
    Version
    0.6.0
    License
    Apache-2.0
    Last updated
    10 hours ago
    Downloads
    2K

    #gaato/http

    The HTTP vocabulary for MoonBit: methods, headers, URLs, requests, responses and bodies, a sans-IO HTTP/1.1 codec, and client and server connection engines that run on ports (connections, clocks, timers) supplied by the caller. It depends on nothing but moonbitlang/core and builds on wasm-gc, wasm, js and native.

    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.

    Sockets, TLS, an event loop and content codings live in gaato/http-async, and the client with retries, cookies and endpoints lives in gaato/mttp.

    #Install

    moon add gaato/http

    #Usage

    Values are immutable: each builder returns a copy.

    ///|
    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")])
    }

    A transport implements Backend: it sends one exchange and returns the response head with a body the caller reads or closes. It refuses an invalid request before any I/O, and every failure is a TransportError whose phase says how far the exchange got, so is_unsent tells whether resending a non-idempotent request is safe. SendOptions says whether the caller reads the whole body inside the attempt (Exchange, with a byte limit) or streams it (Stream, with an idle limit per read).

    ///|
    /// 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()
    }

    Stream bodies are pulled through BodyStream and taken once: a sender that may send a body twice asks Body::is_replayable first.

    #Packages

    PackageStatusWhat it is
    gaato/httpstableThe vocabulary above, Backend and SendOptions, URL, form and HTTP-date helpers
    gaato/http/h1stableThe 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/rtstableThe ports a runtime provides: Clock, Timer, Gate, Conn, Dialer, Signal, Coordination, and default_random
    gaato/http/simstableIn-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/h1clientexperimentalThe 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/h1serverexperimentalThe 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/driverexperimentalA 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

    An experimental package is imported by listing it, as in options("allow-experimental-packages": ["gaato/http/driver"]) in the importer's moon.pkg.

    The root package's default alias @http is also that of moonbitlang/core/async/http. A package that imports both renames one of them, as in "moonbitlang/core/async/http" @corehttp.

    #License

    Apache-2.0

    Handler

    type Handler = async (Request) -> Response noraise

    A server-side handler. Adapters (mttp-net serve, fetch_handler) and the Router all speak this type. WebSocket Upgrade is not in v0.x; a later 101 response can carry the upgrade callback in an attribute.

    Backend

    pub(open) trait Backend {
    async fn send(Self, Request, SendOptions) -> Response raise TransportError
    fn close(Self) -> Unit = _
    }

    A transport for one exchange: it sends the request and returns the response head with a body the caller owns, reads to EOF or closes. HTTP statuses are responses, not errors; a failure to get one is a TransportError carrying the phase it happened in.

    A backend refuses an invalid request before any I/O (Request::validate, SendOptions::validate), moves raw bytes (no redirects, retries, cookies or content decoding), and propagates cancellation unchanged. After close, new sends fail with TransportError(Closed, Pool).

    BodyStream

    pub(open) trait BodyStream {
    async fn read_some(Self) -> Bytes? raise TransportError
    fn close(Self) -> Unit
    }

    A stream with nonempty chunks, sticky EOF and idempotent close.

    UriPart

    pub(open) trait UriPart {
    fn write_uri(Self, UriBuilder) -> Unit
    }

    Types which can contribute URI data. Custom implementations should call write_encoded, or delegate to another UriPart, for untrusted values.
    impl UriPart for Bool
    impl UriPart for Int
    impl UriPart for Int64
    impl UriPart for Double
    impl UriPart for String
    impl UriPart for Option[T]

    TransportError

    pub(all) suberror TransportError {
    TransportError(kind~ : Kind, phase~ : Phase, detail~ : String)
    } derive(
    Debug
    )

    A failure with its category, phase and diagnostic description.

    TransportError::is_unsent

    fn TransportError::is_unsent(self : TransportError) -> Bool

    Whether the failure proves no request bytes reached the origin: it happened in phase Pool or Dial (CONTRACT.md, Backend rule 2), or it is a connect failure or a proxy's refusal, which precede the request. Whether resending is worthwhile is a separate question: an Invalid request is unsent and fails the same way again.

    TransportError::message

    fn TransportError::message(self : TransportError) -> String

    A human-readable diagnostic including category and phase.

    TransportError::to_repr

    Formats a transport failure for diagnostics.

    UrlError

    pub(all) suberror UrlError {
    Invalid(String)
    } derive(Eq,
    Debug
    )

    Why a string is not a URI reference this package accepts.

    UrlError::equal

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

    Compares refusals by their reason.

    UrlError::not_equal

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

    Compares refusals by their reason.

    UrlError::to_repr

    Debug representation of a refused URL.

    AttributeKey

    pub struct AttributeKey[T] {
    // private fields
    }

    sttp-shared AttributeMap without Any: per-key slot plus closures.

    AttributeKey::name

    fn[T] AttributeKey::name(self : AttributeKey[T]) -> String

    Returns the key's diagnostic name.

    AttributeKey::new

    fn[T] AttributeKey::new(name : String) -> AttributeKey[T]

    Creates a distinct typed key; identical names do not alias keys.

    Attributes

    pub struct Attributes {
    // private fields
    }

    Immutable associations of typed keys to local metadata.

    Attributes::empty

    fn Attributes::empty() -> Attributes

    Creates an empty attribute collection.

    Attributes::get

    fn[T] Attributes::get(self : Attributes, key : AttributeKey[T]) -> T?

    Looks up a value with its original type, without consuming it.

    Attributes::put

    fn[T] Attributes::put(self : Attributes, key : AttributeKey[T], value : T) -> Attributes

    Associates a value with a key on an independent copy.

    Body

    pub(all) enum Body {
    Empty
    Bytes(Bytes)
    Stream(StreamBody)
    } derive(Eq,
    Debug
    )

    A request body: bytes in memory, or a stream read as the request is sent.

    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())
    }

    Body::as_bytes

    fn Body::as_bytes(self : Body) -> Bytes?

    The bytes of a body held in memory, or None for a stream (as reqwest's Body::as_bytes).

    Body::equal

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

    Equality: equal bytes, or the same StreamBody.

    Body::is_replayable

    fn Body::is_replayable(self : Body) -> Bool

    Whether a sender may send this body (again): always for bytes, and for a stream only while no transport has taken it.

    Body::length

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

    The number of bytes the body will send, when it is known: the length of the bytes, or the length a stream was declared with.

    Body::not_equal

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

    Equality: equal bytes, or the same StreamBody.

    Body::to_repr

    Debug representation of a body; a stream shows its length, not its bytes.

    BodyTiming

    pub(all) enum BodyTiming {
    Exchange(max_bytes~ : Int)
    Stream(idle_ms~ : Int?)
    } derive(Eq,
    Debug
    )

    How the caller will consume the response body, which decides what the attempt deadline covers and how many bytes the backend admits.

    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))
    }

    BodyTiming::equal

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

    Compares body timings.

    BodyTiming::not_equal

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

    Compares body timings.

    BodyTiming::to_repr

    Formats a body timing for diagnostics.

    Headers

    pub struct Headers {
    // private fields
    } derive(Eq,
    Debug
    )

    Immutable, ordered header pairs with ASCII lowercase names. Equality compares normalized pairs, including their order.

    test {
    let headers = @http.Headers::of([("Accept", "text/plain")])
    assert_eq(headers.get("ACCEPT"), Some("text/plain"))
    }

    Headers::add

    fn Headers::add(self : Headers, name : String, value : String) -> Headers

    Appends one pair, retaining all existing values.

    Headers::contains

    fn Headers::contains(self : Headers, name : String) -> Bool

    Reports whether at least one value exists for a name.

    Headers::each

    fn Headers::each(self : Headers, f : (String, String) -> Unit raise?) -> Unit raise?

    Visits a snapshot of normalized pairs in insertion order.

    Headers::empty

    fn Headers::empty() -> Headers

    Creates an empty header collection.

    Headers::equal

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

    Explicit equality methods for normalized, ordered pairs.

    Headers::get

    fn Headers::get(self : Headers, name : String) -> String?

    Returns the first value for a name, ignoring ASCII case.

    Headers::get_all

    fn Headers::get_all(self : Headers, name : String) -> Array[String]

    Returns a fresh array containing every matching value in insertion order.

    Headers::iter

    fn Headers::iter(self : Headers) -> Iter[(String, String)]

    Iterates over a snapshot of normalized pairs in insertion order.

    Headers::length

    fn Headers::length(self : Headers) -> Int

    Returns the number of pairs, counting repeated names separately.

    Headers::not_equal

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

    Explicit equality methods for normalized, ordered pairs.

    Headers::of

    fn Headers::of(pairs : ArrayView[(String, String)]) -> Headers

    Copies pairs in their original order and normalizes their names.

    Headers::remove

    fn Headers::remove(self : Headers, name : String) -> Headers

    Removes all pairs for a name, ignoring ASCII case.

    Headers::set

    fn Headers::set(self : Headers, name : String, value : String) -> Headers

    Replaces all values for a name at its first position, or appends a new name.

    Headers::to_repr

    Debug representation of the header pairs.

    Headers::validate

    fn Headers::validate(self : Headers) -> Unit raise TransportError

    Checks header names and values before serialization. Empty names and non-token name characters are refused; values cannot contain controls other than horizontal tab, or characters outside the HTTP byte range.

    Kind

    pub(all) enum Kind {
    Invalid
    Connect
    Timeout(ms~ : Int)
    Network
    Protocol
    Proxy(status~ : Int)
    TooLarge(limit~ : Int)
    TooManyRedirects(max~ : Int)
    RedirectRefused
    Unreplayable
    Closed
    } derive(Eq,
    Debug
    )

    Transport failure category, distinct from an HTTP status response.

    Kind::equal

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

    Compares failure categories and their associated limits.

    Kind::not_equal

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

    Compares failure categories and their associated limits.

    Kind::to_repr

    Formats a failure category for diagnostics.

    Method

    pub(all) enum Method {
    Get
    Head
    Post
    Put
    Delete
    Connect
    Options
    Trace
    Patch
    Other(String)
    } derive(Eq, Hash,
    Debug
    )

    Standard HTTP methods and extension tokens.
    impl Show for Method

    Method::equal

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

    Compares HTTP methods.

    Method::hash

    fn Method::hash(self : Method) -> Int

    Hashes an HTTP method.

    Method::hash_combine

    fn Method::hash_combine(Method, Hasher) -> Unit

    Hashes an HTTP method.

    Method::is_idempotent

    fn Method::is_idempotent(self : Method) -> Bool

    Reports methods defined as idempotent by HTTP semantics.

    Method::not_equal

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

    Compares HTTP methods.

    Method::output

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

    Writes the HTTP method token.

    Method::parse

    fn Method::parse(s : String) -> Method

    Parses a case-sensitive method token, retaining unknown tokens verbatim.

    Method::to_repr

    Formats an HTTP method for diagnostics.

    Method::to_string

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

    Returns the wire method token.

    Phase

    pub(all) enum Phase {
    Pool
    Dial
    Write
    Head
    Body
    Unknown
    } derive(Eq,
    Debug
    )

    The exchange phase in which a failure occurred.

    Phase::equal

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

    Compares exchange phases.

    Phase::not_equal

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

    Compares exchange phases.

    Phase::to_repr

    Formats an exchange phase for diagnostics.

    Request

    pub(all) struct Request {
    meth : Method
    url : String
    headers : Headers
    body : Body
    attributes : Attributes
    }

    url is the request target as given: an absolute URL or a relative reference ("/users/1?x=2"). Backends parse it once at send time (Kind::Invalid, phase Pool, so it is unsent); Client joins a relative target onto its base URL by prefix (sdk-runtime semantics, not RFC 3986).

    Request::attribute

    fn[T] Request::attribute(self : Request, key : AttributeKey[T], value : T) -> Request

    Associates typed local metadata with a copy; it is never sent as a header.

    Request::basic

    fn Request::basic(self : Request, username : String, password : String) -> Request

    Sets Basic authorization from UTF-8 username and password bytes. A colon separates the username from the password; it cannot be part of a username.

    Request::bearer

    fn Request::bearer(self : Request, token : String) -> Request

    Sets a bearer Authorization header on a copy.

    Request::body_bytes

    fn Request::body_bytes(self : Request, b : Bytes, content_type? : String) -> Request

    Sets a replayable body on a copy, optionally replacing its content type.

    Request::body_form

    fn Request::body_form(self : Request, pairs : ArrayView[(String, String)]) -> Request

    Encodes pairs with form_urlencode as the body and supplies application/x-www-form-urlencoded only if content-type is absent.

    Request::body_json

    Serializes JSON as UTF-8 with the application/json content type.

    Request::body_stream

    fn Request::body_stream(self : Request, s : &BodyStream, length? : Int64, content_type? : String) -> Request

    Sets a stream that can be acquired once, optionally enforcing its length.

    Request::body_text

    fn Request::body_text(self : Request, s : String) -> Request

    Sets a UTF-8 text body, replacing unpaired surrogates with U+FFFD.

    Request::delete

    fn Request::delete(url : String) -> Request

    Creates a DELETE request.

    Request::get

    fn Request::get(url : String) -> Request

    Creates a GET request.

    Request::head

    fn Request::head(self : Request) -> RequestHead

    Extracts request metadata.

    Request::header

    fn Request::header(self : Request, name : String, value : String) -> Request

    Returns a copy with an additional header, preserving duplicate fields.

    Request::new

    fn Request::new(meth : Method, url : String) -> Request

    Creates a request with empty headers, attributes and body.

    Request::options

    fn Request::options(url : String) -> Request

    Creates an OPTIONS request.

    Request::patch

    fn Request::patch(url : String) -> Request

    Creates a PATCH request.

    Request::post

    fn Request::post(url : String) -> Request

    Creates a POST request.

    Request::put

    fn Request::put(url : String) -> Request

    Creates a PUT request.

    Request::query

    fn Request::query(self : Request, name : String, value : String) -> Request

    Appends a percent-encoded query parameter to a copy.

    Request::set_header

    fn Request::set_header(self : Request, name : String, value : String) -> Request

    Returns a copy replacing every header of this name at its first position.

    Request::validate

    fn Request::validate(self : Request) -> Unit raise TransportError

    Validates the method token, raw URL, headers and declared stream length before a backend sends it. A stream body declaring a negative length is refused here as StreamBody::take would refuse it, and its source is closed, since no sender will take it.

    RequestHead

    pub(all) struct RequestHead {
    meth : Method
    url : String
    headers : Headers
    } derive(Eq,
    Debug
    )

    Request metadata without its body or local attributes.

    RequestHead::equal

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

    Compares request metadata.

    RequestHead::not_equal

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

    Compares request metadata.

    RequestHead::to_repr

    Formats request metadata for diagnostics.

    Response

    pub(all) struct Response {
    status : Int
    headers : Headers
    body : Body
    attributes : Attributes
    }

    One response type for both directions. A server closes the connection after a response carrying Connection: close (413 does). attributes is the response-side channel: FollowRedirects records the final URL and history there, and the router records the matched route template.

    Response::close

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

    Releases a streaming response body; empty and byte bodies need no cleanup.

    Response::close_if_unclaimed

    fn Response::close_if_unclaimed(self : Response) -> Unit

    Closes a discarded response only while its streamed reader is unclaimed. StreamBody::take transfers ownership to the returned reader, so a lazy middleware wrapper that took it remains responsible for closing it.

    Response::closes_connection

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

    Reports a close token across all Connection header values.

    Response::head

    fn Response::head(self : Response) -> ResponseHead

    Extracts response metadata.

    Response::new

    fn Response::new(status : Int, headers? : Headers, body? : Body) -> Response

    Creates a response, defaulting to empty headers and body.

    ResponseHead

    pub(all) struct ResponseHead {
    status : Int
    headers : Headers
    } derive(Eq,
    Debug
    )

    Response status and headers without the body.

    ResponseHead::equal

    Compares response metadata.

    ResponseHead::not_equal

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

    Compares response metadata.

    ResponseHead::to_repr

    Formats response metadata for diagnostics.

    SendOptions

    pub(all) struct SendOptions {
    timeout_ms : Int?
    body : BodyTiming
    } derive(Eq,
    Debug
    )

    The per-send settings a caller hands a Backend. Build them from SendOptions::default() with a record update; the field set is frozen.

    SendOptions::default

    fn SendOptions::default() -> SendOptions

    No attempt deadline and a buffered body of at most 64 MiB on the wire.

    SendOptions::equal

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

    Compares send options.

    SendOptions::not_equal

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

    Compares send options.

    SendOptions::to_repr

    Formats send options for diagnostics.

    SendOptions::validate

    fn SendOptions::validate(self : SendOptions) -> Unit raise TransportError

    Refuses options no send can honour, before any I/O and so unsent: a nonpositive timeout_ms raises Timeout(ms) at once, and a negative Exchange(max_bytes) or a nonpositive Stream(idle_ms) raises Invalid, all in phase Pool. Exchange(max_bytes=0) is valid and admits only an empty body; an idle limit of zero is refused because no read could ever meet it.

    test {
    @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")
    }
    }

    StreamBody

    pub struct StreamBody {
    // private fields
    }

    A request body read once from a BodyStream, with its length when known.

    A transport sends a known length as Content-Length and an unknown one, on HTTP/1.1, as Transfer-Encoding: chunked. The transport that sends the body takes it (take) and closes the stream when it is done with it, sent or failed; a stream that is never taken stays open and is its creator's to close. Two stream bodies are equal only when they are the same one.
    impl Eq for StreamBody
    impl Debug for StreamBody

    StreamBody::close

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

    Closes the source and prevents future acquisition.

    StreamBody::equal

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

    Identity: the same stream body, not an equal one.

    StreamBody::length

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

    The declared length, if any.

    StreamBody::new

    fn StreamBody::new(source : &BodyStream, length? : Int64) -> StreamBody

    Wraps source, declared to produce exactly length bytes when given.

    StreamBody::not_equal

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

    Identity: the same stream body, not an equal one.

    StreamBody::take

    fn StreamBody::take(self : StreamBody) -> &BodyStream raise TransportError

    Hands the stream to the one sender that reads it. The returned stream raises Protocol in phase Body when the source produces more bytes than the declared length, ends before it, or yields an empty chunk; it closes the source then, at EOF, and on close.

    A second call raises Unreplayable in phase Pool, since the bytes already read are gone. A negative declared length raises Invalid in phase Pool before anything is read, and closes the source, which no sender will take.

    StreamBody::to_repr

    Debug representation of a stream body.

    Trusted

    pub(all) struct Trusted(String)

    Explicit, already-encoded URI syntax. Unlike String interpolation, this may introduce separators, a scheme or an authority; only use trusted data.
    impl UriPart for Trusted

    Trusted::write_uri

    fn Trusted::write_uri(self : Trusted, builder : UriBuilder) -> Unit

    Writes this explicitly trusted fragment to a URI builder.

    UriBuilder

    pub struct UriBuilder {
    // private fields
    }

    An interpolation builder for URI strings. Literal syntax is preserved; interpolated values are percent-encoded as individual components. Query parameters are staged until their separator so None can omit the whole parameter without retracting or repeatedly copying the preceding URL.

    UriBuilder::new

    fn UriBuilder::new() -> UriBuilder

    Creates an empty URI builder; use uri for interpolation and finalization.

    UriBuilder::to_string

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

    Returns the current URI without mutating the builder. Empty query syntax and parameters omitted by None do not leave stray separators.

    UriBuilder::write_encoded

    fn UriBuilder::write_encoded(self : UriBuilder, value : String) -> Unit

    Writes a component, escaping every character outside RFC 3986 unreserved syntax. Even at the start, a plain String cannot inject a scheme or host.

    UriBuilder::write_string

    fn UriBuilder::write_string(self : UriBuilder, text : String) -> Unit

    Writes URI syntax literally. Reserved characters in interpolated data should instead go through interpolation or write_encoded.

    UriBuilder::write_string_interpolation

    fn[T : UriPart] UriBuilder::write_string_interpolation(self : UriBuilder, value : T) -> Unit

    Writes an interpolated value using its UriPart implementation.

    Url

    pub struct Url {
    scheme : String?
    userinfo : String?
    host : String?
    port : Int?
    path : String
    query : String?
    fragment : String?
    } derive(Eq,
    Debug
    )

    A URI reference (RFC 3986 §4.1), held normalised and percent-encoded.

    Url::parse lower-cases the scheme and host, writes an IPv6 address in RFC 5952's canonical hex form (a dotted-quad tail too, as WHATWG URL does), drops a scheme's default port (http 80, https 443, ws 80, wss 443, ftp 21), percent-encodes what the WHATWG URL Standard's sets encode (non-ASCII as UTF-8), and removes dot segments from the path of a reference with a scheme. Existing escapes are kept as written. No scheme makes the reference relative; no host means no authority, which is not an empty host. A host is ASCII only: internationalised domain names (IDNA) are refused, so pass their xn-- form.

    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")
    }
    impl Show for Url

    Url::append_query_params

    fn Url::append_query_params(self : Url, params : Array[(String, String)]) -> Url

    Appends encoded pairs after the existing query, keeping it as written. Names and values keep only RFC 3986 unreserved characters, as percent_encode does.

    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")
    }

    Url::equal

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

    Compares all components; parsing has already normalised them.

    Url::is_absolute

    fn Url::is_absolute(self : Url) -> Bool

    Reports whether the reference has a scheme.

    Url::merge_query_params

    fn Url::merge_query_params(self : Url, params : Array[(String, String)]) -> Url

    Replaces every existing pair whose decoded name is among params' names, then appends params in order, as httpx's merge_params does. The pairs kept are kept as written.

    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")
    }

    Url::not_equal

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

    Compares all components; parsing has already normalised them.

    Url::output

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

    The serialised reference.

    Url::parse

    fn Url::parse(text : String) -> Url raise UrlError

    Parses a URI reference, absolute or relative.

    Leading and trailing C0 controls and spaces are trimmed and tabs and line breaks removed, as browsers do. Only the authority can be refused: a port that is not a decimal number up to 65535, an IP literal that is not an IPv6 address (zone identifiers and IPvFuture included), or a host with a character outside RFC 3986's reg-name — non-ASCII and percent-encoded hosts among them, since IDNA is out of scope. A backslash is data, not a separator, and is percent-encoded.

    Url::path_segments

    fn Url::path_segments(self : Url) -> Array[String] raise UrlError

    Decoded path segments, for routing.

    Url::port_or_default

    fn Url::port_or_default(self : Url) -> Int?

    The port, or the scheme's default when the URL names none.

    Url::query_params

    fn Url::query_params(self : Url) -> Array[(String, String)]

    The query's name-value pairs, decoded as application/x-www-form-urlencoded: + is a space, escapes are UTF-8 (malformed ones are kept literally), empty pairs are skipped and a pair without = has an empty value.

    Url::resolve

    fn Url::resolve(self : Url, reference : Url) -> Url

    Resolves reference against this base (RFC 3986 §5.2.2, strict): a reference with a scheme, even the base's own, stays absolute. The base should be absolute; RFC 3986 defines no result otherwise.

    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")
    }

    Url::same_origin

    fn Url::same_origin(self : Url, other : Url) -> Bool

    Reports whether both URLs have the same scheme, host and port, counting the scheme's default port. A URL without a scheme or host has no origin and matches nothing.

    Url::to_repr

    Debug representation of the components.

    Url::to_string

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

    The serialised reference.

    Url::without_fragment

    fn Url::without_fragment(self : Url) -> Url

    The same URL without its fragment, which is never sent to a server.

    append_query

    fn append_query(url : String, name : String, value : String) -> String

    Appends an encoded query pair, using ampersand if the URL contains ?.

    background

    let background : AttributeKey[(async () -> Unit) -> Unit]

    Adapter-owned scheduling for work that may outlive the response. Invoke during request handling; the adapter owns completion and shutdown policy.

    encode_path

    fn encode_path(segments : Array[String]) -> String

    Encodes path segments for a request target ("/users/a%2Fb").

    encode_utf8

    fn encode_utf8(text : String) -> Bytes

    Encodes text as UTF-8, writing each lone surrogate as U+FFFD, as the WHATWG Encoding Standard's UTF-8 encoder and TextEncoder do. @utf8.encode aborts the process on a lone surrogate instead, so it is unsafe for strings that came from outside.

    test {
    let lone = String::from_array([(0xD800).unsafe_to_char()])
    assert_eq(@http.encode_utf8("é" + lone), b"\xC3\xA9\xEF\xBF\xBD")
    }

    form_urldecode

    fn form_urldecode(input : String, max_pairs? : Int) -> Array[(String, String)] raise TransportError

    Decodes application/x-www-form-urlencoded pairs in order, preserving duplicates. Empty pieces are skipped, a missing equals means empty value, plus becomes space, malformed escapes stay literal and invalid UTF-8 is replaced. At most max_pairs (default 256) nonempty pairs are accepted. Byte limits belong to the caller that reads the input body.

    form_urlencode

    fn form_urlencode(pairs : ArrayView[(String, String)]) -> String

    Serializes name/value pairs, in order, as an application/x-www-form-urlencoded body with the WHATWG URL Standard's urlencoded serializer, as browsers and URLSearchParams do: a space becomes +, and every UTF-8 byte other than an ASCII alphanumeric or one of *-._ is percent-encoded in uppercase hex. This differs from percent_encode, which keeps ~, encodes * and writes a space as %20. A lone surrogate is encoded as U+FFFD.

    test {
    assert_eq(
    @http.form_urlencode([("q", "a b&c"), ("lang", "日本")]),
    "q=a+b%26c&lang=%E6%97%A5%E6%9C%AC",
    )
    }

    is_idempotent

    fn is_idempotent(req : Request) -> Bool

    An idempotent method or an explicit replay_safe statement. A header alone cannot establish the remote service's deduplication contract.

    is_success

    fn is_success(status : Int) -> Bool

    Reports whether a status belongs to the successful 2xx class.

    join_url

    fn join_url(base : String, target : String) -> String

    Base-URL join by prefix (sdk-runtime's base_url semantics): an absolute http(s) target passes through; otherwise one slash is kept between base and target. "https://api.openai.com/v1" + "/chat" gives "https://api.openai.com/v1/chat" (RFC 3986 resolve would drop /v1).

    parse_http_date

    fn parse_http_date(s : String, now_unix_ms~ : Int64) -> Int64?

    Parses an HTTP-date (RFC 9110 §5.6.7) into Unix milliseconds: the IMF-fixdate a sender generates, or one of the two obsolete forms a recipient must also accept, rfc850-date and asctime-date. HTTP-date is case-sensitive and the input is not trimmed. The weekday must agree with the date. now_unix_ms places rfc850-date's two-digit year: a year more than 50 years after it is taken from the century before.

    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)
    }

    percent_decode

    fn percent_decode(s : String) -> String raise UrlError

    Decodes percent-encoded UTF-8, refusing malformed escapes or encoding.

    percent_encode

    fn percent_encode(value : String) -> String

    Percent-encodes UTF-8 bytes, preserving only RFC 3986 unreserved bytes.

    percent_encode_path

    fn percent_encode_path(value : String) -> String

    Percent-encodes a multi-segment path, also preserving slash separators.

    read_bounded

    async fn read_bounded(stream : &BodyStream, limit : Int) -> Bytes raise TransportError

    Reads and closes a stream, refusing the first chunk that crosses limit. Subtraction before comparison prevents accumulated-length overflow.

    reason_phrase

    fn reason_phrase(status : Int) -> String

    Returns a conventional phrase for common statuses, or an empty string.

    remote_addr

    let remote_addr : AttributeKey[String]

    Set by server adapters.

    replay_safe

    let replay_safe : AttributeKey[Bool]

    A caller's statement that resending this request is safe even after it was written (discord's REPLAY_SAFE_HEADER, without a wire header).

    uri

    fn uri(build : (UriBuilder) -> Unit) -> String

    Builds a URI string without parsing it. Send-time validation still applies.

    test {
    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",
    )
    }

    validate_url

    fn validate_url(url : String) -> Unit raise TransportError

    Validates the original URL before normalization can encode an invalid request target. Unicode text is permitted and encoded by the URL parser.