#gaato/mttp/client

    The HTTP client of gaato/mttp: a description of what to send and how to read the answer, and a Client that sends it through any @http.Backend with defaults, admission, deadlines, decoded-body limits and retries. It does no I/O itself; gaato/http-async provides the backends that do, and gaato/mttp/testing the stubs.

    #Requests and response plans

    ResponsePlan::prepared_stream(decode, use_) supports a buffered interpretation followed by an async callback. It bounds and decodes bytes inside the attempt. The decoder's Err(final_value) reaches the retry decision immediately; its Ok(value) hands the decoded value to use_ after release, observation and retry decisions. The callback runs once and its exceptions remain effects. This is the endpoint interpreter's bridge for buffered call_with outputs.

    A RequestPlan[T, M] pairs an @http.Request with a ResponsePlan[T, M], which says how to turn the response into a T. The mode M says when the body is read:

    • Buffered plans are read inside the attempt, so a rejected status or an undecodable body reaches the retry decision: as_bytes, as_string, as_json, as_json_value, as_form, ignore_body, ResponsePlan::new, ResponsePlan::reject and as_both.
    • Streaming plans hand the body to a callback after the attempt, and that callback is never retried: as_stream, as_sse, as_lines and ResponsePlan::stream.
    • from_head chooses a plan once the head is known; as_streaming widens a Buffered plan, never the reverse.

    Expected failures are values. The built-in readers return Result[_, ResponseFailure]: UnexpectedStatus for a rejected status and Deserialization for a body that does not decode, each with the real head and at most 1 MiB of the body. A TransportError, a cancellation and any other error stay raised, with their identity.

    #The client loop

    Client::send (or RequestPlan::send) is the only sending path. It joins the base_url, merges default headers under the request's own, then for each attempt acquires the RateLimiter, builds the body (body_fn runs after admission, so a retry gets fresh bytes), sends, releases the limiter with that acquire's opaque Admission and the head it got, reports one Attempt to the observer and asks the RetryPolicy, or a typed decide_retry that also sees the policy's default. Retrying is opt-in (RetryPolicy::none() is the default), a stream body is never resent without a body factory, and idempotency comes from the method or the replay_safe attribute.

    Client::close() records closure and closes the shared backend idempotently. If an observer closes the client after a retry decision, the loop returns the last attempt's reply or raised error without sleeping or acquiring again. Closure during backoff takes effect when the injected clock's sleep wakes; it returns that same outcome without another acquire or attempt. An accepted stream is still handed to its consumer as with Stop. The completed attempt keeps its paired release. Sends begun after closure fail with Closed in phase Pool before admission.

    Every successful acquire returns an Admission, including its logical bucket and issuer-assigned sequence. The Client retains it through success, failure and cancellation cleanup; each retry acquires its own token. Custom limiters construct tokens with Admission::new(bucket, sequence=...) and inspect bucket() / sequence() on release. Tokens belong to their issuing limiter; implementations must ignore duplicate releases and bound outstanding identities.

    Each attempt maps the plan onto the @http.SendOptions the backend gets:

    PlanbodyThe Client's reader
    BufferedExchange(max_bytes = max_body_bytes): the deadline covers the body, and the backend admits at most that many wire byteskeeps at most max_body_bytes decoded bytes
    Streaming, from_head includedStream(idle_ms = read_idle_ms): the deadline ends at the head, each read gets the idle limita buffered branch the head selects keeps max_body_bytes and the rest of the attempt deadline, enforced with the Client's Timer

    timeout_ms, max_body_bytes and read_idle_ms are Client defaults that a plan overrides field by field. total_ms bounds the whole call, admission, retry waits and the stream consumer included; it requires a Timer.

    ///|
    test "retry a 503 and read JSON" {
    // A backend that is busy once, then answers.
    let mut calls = 0
    let backend = @client.from_fn((_, _) => {
    calls += 1
    if calls == 1 {
    @http.Response::new(503)
    } else {
    @http.Response::new(200, body=Bytes(b"{\"id\":7}"))
    }
    })
    let client = @client.Client::new(
    backend,
    @sim.FakeClock::new(),
    base_url="https://api.example/v1",
    retry=@client.RetryPolicy::conservative(),
    )
    let plan : @client.ClientRequest[Result[Json, @client.ResponseFailure]] = @client.get(
    "/items/7",
    ).response(@client.as_json_value())
    let reply = @driver.run_sync(() => client.send(plan))
    assert_eq(reply.body, Ok({ "id": 7 }))
    assert_eq(reply.request.url, "https://api.example/v1/items/7")
    assert_eq(calls, 2)
    }

    #Wrappers

    A policy that needs only the request and the response head is a Backend wrapper:

    • FollowRedirects follows 301, 302, 303, 307 and 308 under a RedirectConfig (10 hops, no HTTPS downgrade, credentials removed on an origin change), overridden per request by the redirect_limit attribute that RequestPlan::follow_redirects and max_redirects set. It records redirect_history and final_url. Keep retries above it; a transport failure on a later hop never reads as unsent, since the first request was answered.
    • from_fn and from_handler turn a function or an @http.Handler into a backend that validates what it is given.

    #Helpers

    Paginator walks cursor-paged APIs with failures as values; parse_link_next reads an RFC 8288 Link: <…>; rel="next"; parse_retry_after and retry_after_ms read server-requested delays; RequestPlan::to_curl renders a request for diagnostics with credentials redacted.

    AttemptOutcome

    The result of an attempt that did not succeed: a status or an TransportError (from gaato/mttp/client/retry).

    Backoff

    Exponential backoff between attempts, in milliseconds (from gaato/mttp/client/retry).

    ClientRequest

    type ClientRequest[T] = RequestPlan[T, Buffered]

    A request whose response is buffered before it is returned.

    Decision

    What to do after a failed attempt: wait and retry, or stop (from gaato/mttp/client/retry).

    Jitter

    How a backoff delay is spread so clients do not retry in lockstep (from gaato/mttp/client/retry).

    Methods

    The request methods a retry is allowed for (from gaato/mttp/client/retry).

    ResponseAs

    type ResponseAs[T] = ResponsePlan[T, Buffered]

    A buffered response description.

    RetryAfter

    How a server-requested delay is read and bounded (from gaato/mttp/client/retry).

    RetryAfterCap

    What to do with a server-requested delay over a limit (from gaato/mttp/client/retry).

    RetryAfterSource

    A header a server-requested delay is read from (from gaato/mttp/client/retry).

    RetryRule

    A retry rule of gaato/mttp/client/retry: which failures to retry, how long to wait, and how often.

    StatusMatch

    One status code or an inclusive range of them, in a StatusRule (from gaato/mttp/client/retry).

    StatusRule

    Response statuses a RetryRule retries, and for which methods (from gaato/mttp/client/retry).

    StopReason

    Why a RetryRule stopped retrying (from gaato/mttp/client/retry).

    StreamRequest

    type StreamRequest[T] = RequestPlan[T, Streaming]

    A request whose response may run a scoped stream consumer.

    StreamResponseAs

    type StreamResponseAs[T] = ResponsePlan[T, Streaming]

    A response description with a scoped live-stream consumer.

    RateLimiter

    pub(open) trait RateLimiter {
    async fn acquire(Self, String, global_exempt~ : Bool) -> Admission
    async fn release(Self, Admission, status~ : Int, headers~ :
    Headers
    ) -> Unit
    }

    Admission control for logical request buckets. Every completed acquire has one release with that acquire's Admission, including cancellation and decoding failures. Implementations ignore repeated releases of a token.

    ResponseMode

    pub trait ResponseMode {
    fn fresh() -> Self
    fn may_stream(Self) -> Bool
    }

    Closed set of response-lifetime markers used by typed descriptions.

    InvalidRequest

    pub(all) suberror InvalidRequest {
    InvalidRequest(message~ : String)
    } derive(
    Debug
    )

    A request encoder rejected input before anything was sent.

    InvalidRequest::to_repr

    Formats request encoding failures for diagnostics.

    Admission

    pub struct Admission {
    // private fields
    }

    An admission issued by a RateLimiter. Return it only to its issuing limiter. Copies identify the same admission; a second release is ignored.

    Admission::bucket

    fn Admission::bucket(self : Admission) -> String

    The logical request bucket passed to acquire.

    Admission::new

    fn Admission::new(bucket : String, sequence~ : Int64) -> Admission

    Creates a token for a custom limiter. The issuer assigns a strictly increasing sequence and tracks outstanding tokens to ignore duplicate releases.

    Admission::sequence

    fn Admission::sequence(self : Admission) -> Int64

    The issuer's admission sequence, independent of response delivery time.

    Attempt

    pub(all) struct Attempt {
    request :
    RequestHead

    attributes :
    Attributes

    bucket : String
    attempt : Int
    status : Int
    headers :
    Headers

    duration_ms : Int64
    error : Error?
    decision :
    Decision
    ?
    }

    A settled attempt, reported after releasing admission. Attempt zero is the initial send; status zero means no response head arrived.

    AttemptResult

    pub(all) enum AttemptResult[T] {
    Buffered(Reply[T])
    StreamReady(
    ResponseHead
    )
    TransportFailure(
    TransportError
    )
    }

    A typed value or transport failure offered before any stream handoff.

    AttemptResult::http_outcome

    Projects HTTP facts for a generic retry rule without inspecting T.

    Buffered

    pub(all) struct Buffered {
    }

    Buffered response processing, including a stream buffered by Client.

    Buffered::fresh

    #deprecated("`Buffered::fresh` is deprecated, use `ResponseMode::fresh` instead.")
    fn Buffered::fresh() -> Buffered

    Keeps the trait's operations from being promoted to methods of the marker (#deprecated on an extend declares that the promotion is unwanted).

    Buffered::may_stream

    #deprecated("`Buffered::may_stream` is deprecated, use `ResponseMode::may_stream` instead.")
    fn Buffered::may_stream(_ : Buffered) -> Bool

    Keeps the trait's operations from being promoted to methods of the marker (#deprecated on an extend declares that the promotion is unwanted).

    Client

    pub struct Client {
    // private fields
    }

    A backend plus shared request defaults, policies and injected timing services. Client::send is the only sending path: it applies the defaults, admission, deadlines, decoded-body limits and retries, and maps each plan onto the SendOptions the backend receives.

    Client::close

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

    Closes the shared backend idempotently and stops retries. A planned retry returns its last outcome if closed before its wait or when that wait wakes, without another limiter acquisition. Closing does not interrupt Clock.sleep. Later sends fail with Closed in phase Pool before admission.

    Client::new

    fn Client::new(backend : &
    Backend
    , clock : &
    Clock
    , base_url? : String, headers? :
    Headers
    , timeout_ms? : Int, max_body_bytes? : Int, read_idle_ms? : Int, timer? : &
    Timer
    , retry? : RetryPolicy, limiter? : &RateLimiter, total_ms? : Int, observer? : (Attempt) -> Unit, random? : () -> Double) -> Client

    Creates a client over backend, reading time from clock.

    • base_url is prefixed to each request URL with @http.join_url (a prefix join, never RFC 3986 resolution); headers are defaults, and a header the request sets itself wins.
    • timeout_ms is the attempt deadline, sent as SendOptions.timeout_ms.
    • max_body_bytes (default 64 MiB) bounds the decoded bytes a buffered reader keeps, and is sent as Exchange(max_bytes) for a Buffered plan, so the backend bounds the wire bytes with the same number.
    • read_idle_ms is the per-read deadline sent as Stream(idle_ms) for a Streaming plan.
    • timer lets the Client enforce deadlines itself: the attempt deadline around each attempt (for backends that cannot, and for a buffered read that a Streaming plan selects from the head), and the whole-call total_ms, which requires it.
    • retry (default: none), limiter (default: admits at once), observer (gets one Attempt per attempt) and random (for jitter).

    Client::send

    async fn[T, M : ResponseMode] Client::send(self : Client, request : RequestPlan[T, M], decide_retry? : (AttemptResult[T], RetryContext) ->
    Decision
    ) -> Reply[T]

    Sends a typed description through the one executor.

    Before admission, the request and its limits are validated, and a total deadline requires a Timer. Each attempt acquires the limiter, builds the body (body_fn runs after admission), sends with SendOptions mapped from the plan, captures the head and reads a buffered branch; it then releases the limiter with that head, reports one Attempt and asks the retry policy (or decide_retry, which also sees the policy's default decision). A selected stream consumer starts only after that, and its result or exception is never retried. Closing the client stops a planned retry before sleeping or, if already asleep, on waking, and returns the last attempt's reply or raised error.

    FollowRedirects

    pub struct FollowRedirects {
    // private fields
    }

    Follows 301, 302, 303, 307 and 308 responses. Intermediate bodies close unread; credentials are removed when the origin changes; an HTTPS downgrade is refused unless configured. A 303, or a 301 or 302 to a POST, becomes a GET without the body; any other hop resends the body, so a body that cannot be sent again is refused (Unreplayable).

    The wrapper passes SendOptions to every hop unchanged, so each hop gets the attempt deadline; a Client with a Timer bounds the whole chain. Place Cookies inside this wrapper so every hop is processed, and keep retries above it: a 503 on the second hop re-sends the first.

    A transport failure on a later hop is never reported as unsent: the first request got an answer, so it reached the server. Such a failure has phase Unknown, and a Connect or Proxy kind becomes Network; the detail keeps the original.

    FollowRedirects::close

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

    Closes the wrapped backend at most once.

    FollowRedirects::new

    fn FollowRedirects::new(inner : &
    Backend
    , config? : RedirectConfig, on_redirect? : (RedirectHop) -> Unit) -> FollowRedirects

    Wraps a backend. on_redirect sees each followed hop after its response was closed.

    FollowRedirects::send

    Follows redirects and records the final URL (final_url) and the followed hops (redirect_history) on the returned response.

    NoLimiter

    pub struct NoLimiter {
    // private fields
    }

    Admission without throttling or retained state.

    NoLimiter::acquire

    async fn NoLimiter::acquire(self : NoLimiter, bucket : String, global_exempt~ : Bool) -> Admission

    Calls admission and release through method syntax.

    NoLimiter::new

    fn NoLimiter::new() -> NoLimiter

    Creates an admission controller that admits immediately.

    NoLimiter::release

    async fn NoLimiter::release(_ : NoLimiter, _ : Admission, status~ : Int, headers~ :
    Headers
    ) -> Unit

    Calls admission and release through method syntax.

    Page

    pub(all) struct Page[T] {
    items : Array[T]
    next : String?
    }

    One decoded page and its optional cursor for the following page.

    Paginator

    pub struct Paginator[T, E] {
    // private fields
    }

    A stateful paginator with expected failures as values. Transport errors remain effects; a failed fetch never advances the cursor.

    Paginator::collect

    async fn[T, E] Paginator::collect(self : Paginator[T, E], max~ : Int) -> Result[Array[T], E]

    Collects at most max items, keeping unused items from the last page. A non-positive maximum performs no requests.

    Paginator::each

    async fn[T, E] Paginator::each(self : Paginator[T, E], f : async (T) -> Result[Unit, E]) -> Result[Unit, E]

    Visits each item in page order, stopping at the first expected failure.

    Paginator::new

    fn[T, E] Paginator::new(fetch : async () -> Result[Array[T]?, E]) -> Paginator[T, E]

    Creates a paginator whose fetch owns its cursor. None or an empty page ends iteration. The fetch must advance its cursor only after a successful page.

    Paginator::next_page

    async fn[T, E] Paginator::next_page(self : Paginator[T, E]) -> Result[Array[T]?, E]

    Fetches the next non-empty page, preserving unused items from collect.

    Paginator::with_cursor

    fn[T, E] Paginator::with_cursor(fetch : async (String?) -> Result[Page[T], E]) -> Paginator[T, E]

    Creates a paginator which advances its string cursor only on Ok.

    RedirectConfig

    pub(all) struct RedirectConfig {
    max_redirects : Int
    allow_downgrade : Bool
    credential_headers : Array[String]
    } derive(Eq,
    Debug
    )

    How FollowRedirects follows. Build it from RedirectConfig::default() with a record update; the field set is frozen.

    RedirectConfig::default

    Ten hops, no downgrade, and authorization, proxy-authorization and cookie as the credentials removed on an origin change.

    test {
    let config = {
    ..@client.RedirectConfig::default(),
    credential_headers: [
    "authorization", "proxy-authorization", "cookie", "x-api-key",
    ],
    }
    assert_eq(config.max_redirects, 10)
    assert_false(config.allow_downgrade)
    }

    RedirectConfig::equal

    Compares redirect configurations.

    RedirectConfig::not_equal

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

    Compares redirect configurations.

    RedirectConfig::to_repr

    Formats a redirect configuration for diagnostics.

    RedirectHop

    A followed redirect's metadata, without retaining either request or response bodies. Its response stream is closed before this is reported.

    RedirectHop::equal

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

    Compares redirect metadata.

    RedirectHop::not_equal

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

    Compares redirect metadata.

    RedirectHop::to_repr

    Formats redirect metadata.

    Reply

    pub(all) struct Reply[T] {
    status : Int
    headers :
    Headers

    body : T
    request :
    RequestHead

    attributes :
    Attributes

    }

    A decoded response, preserving the actual HTTP and request metadata.

    RequestPlan

    pub struct RequestPlan[T, M] {
    // private fields
    }

    A wire request, response description and per-request options.

    RequestPlan::attribute

    fn[T, A, M] RequestPlan::attribute(self : RequestPlan[T, M], key :
    AttributeKey
    [A], value : A) -> RequestPlan[T, M]

    Adds typed local metadata to a copy.

    RequestPlan::bearer

    fn[T, M] RequestPlan::bearer(self : RequestPlan[T, M], token : String) -> RequestPlan[T, M]

    Sets a bearer Authorization header on a copy.

    RequestPlan::body_bytes

    fn[T, M] RequestPlan::body_bytes(self : RequestPlan[T, M], bytes : Bytes) -> RequestPlan[T, M]

    Sets a replayable byte body on a copy.

    RequestPlan::body_fn

    fn[T, M] RequestPlan::body_fn(self : RequestPlan[T, M], factory : () -> Bytes) -> RequestPlan[T, M]

    Supplies bytes freshly built after admission on each attempt.

    RequestPlan::body_json

    fn[T, M] RequestPlan::body_json(self : RequestPlan[T, M], value :
    Json
    ) -> RequestPlan[T, M]

    Sets a JSON body on a copy.

    RequestPlan::follow_redirects

    fn[T, M] RequestPlan::follow_redirects(self : RequestPlan[T, M], enabled : Bool) -> RequestPlan[T, M]

    Turns redirect following off for this request (false sets the redirect_limit attribute to 0), or, with true, undoes an earlier follow_redirects(false) on this plan so the FollowRedirects configuration applies again.

    RequestPlan::header

    fn[T, M] RequestPlan::header(self : RequestPlan[T, M], name : String, value : String) -> RequestPlan[T, M]

    Adds a wire header to a copy of the request.

    RequestPlan::max_body_bytes

    fn[T, M] RequestPlan::max_body_bytes(self : RequestPlan[T, M], bytes : Int) -> RequestPlan[T, M]

    Overrides the Client's body limit for this request: the decoded bytes a buffered reader keeps, and the wire bytes sent as Exchange(max_bytes).

    RequestPlan::max_redirects

    fn[T, M] RequestPlan::max_redirects(self : RequestPlan[T, M], max : Int) -> RequestPlan[T, M]

    Overrides the redirect-hop limit of FollowRedirects for this request, through the redirect_limit attribute. A limit below 1 follows nothing.

    RequestPlan::new

    fn[T, M] RequestPlan::new(raw :
    Request
    , response_as : ResponsePlan[T, M]) -> RequestPlan[T, M]

    Pairs a wire request with its response interpretation.

    RequestPlan::raw

    fn[T, M] RequestPlan::raw(self : RequestPlan[T, M]) ->
    Request

    Returns the wire request before client defaults are applied.

    RequestPlan::read_idle_ms

    fn[T, M] RequestPlan::read_idle_ms(self : RequestPlan[T, M], ms : Int) -> RequestPlan[T, M]

    Overrides the Client's per-read deadline for a body sent as Stream(idle_ms).

    RequestPlan::response

    fn[T, U, M, N] RequestPlan::response(self : RequestPlan[T, M], response_as : ResponsePlan[U, N]) -> RequestPlan[U, N]

    Replaces the interpretation, including its lifetime marker.

    RequestPlan::send

    async fn[T, M : ResponseMode] RequestPlan::send(self : RequestPlan[T, M], client : Client, decide_retry? : (AttemptResult[T], RetryContext) ->
    Decision
    ) -> Reply[T]

    Sends through the Client's single executor, with an optional typed policy.

    RequestPlan::timeout_ms

    fn[T, M] RequestPlan::timeout_ms(self : RequestPlan[T, M], ms : Int) -> RequestPlan[T, M]

    Overrides the Client's attempt deadline for this request. It is sent as SendOptions.timeout_ms, shortened by what is left of a total deadline.

    RequestPlan::to_curl

    fn[T] RequestPlan::to_curl(self : RequestPlan[T, Buffered], sensitive_headers? : Array[String], include_body? : Bool) -> String

    Renders a POSIX-shell curl diagnostic for the raw request, before Client defaults, cookies, retries or redirects. Sensitive header names are matched case-insensitively. URL userinfo is redacted; query strings and byte bodies are not. Set include_body=false to omit byte bodies as well.

    Byte bodies use a quoted printf pipeline so NUL, quotes, newlines and an initial @ remain bytes rather than shell syntax or a curl filename. Stream bodies and body factories are never consumed/invoked; a comment marks their omission. This function does not execute the command or validate the URL.

    ResponseFailure

    pub(all) enum ResponseFailure {
    UnexpectedStatus(
    ResponseHead
    , body~ : Bytes)
    Deserialization(
    ResponseHead
    , message~ : String, body~ : Bytes)
    } derive(Eq,
    Debug
    )

    A received response that is not the value the description asked for. It is a value: readers return it as Err, and transport failures stay raised TransportErrors.

    ResponseFailure::equal

    Compares response interpretation failures.

    ResponseFailure::head

    Returns the received response metadata.

    ResponseFailure::not_equal

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

    Compares response interpretation failures.

    ResponseFailure::to_repr

    Formats response failures for diagnostics.

    ResponsePlan

    pub struct ResponsePlan[T, M] {
    // private fields
    }

    An immutable response description. Selection after the head arrives decides the actual lifetime; the marker describes possible streaming.

    ResponsePlan::any_status

    fn[T, M] ResponsePlan::any_status(self : ResponsePlan[T, M]) -> ResponsePlan[T, M]

    Interprets every status through the successful branch of this description.

    ResponsePlan::as_streaming

    fn[T] ResponsePlan::as_streaming(self : ResponsePlan[T, Buffered]) -> ResponsePlan[T, Streaming]

    Widens a buffered description without changing its selected reader.

    ResponsePlan::map

    fn[A, B, M] ResponsePlan::map(self : ResponsePlan[A, M], f : (A) -> B raise) -> ResponsePlan[B, M]

    Transforms the full interpreted value. Unknown mapping errors propagate.

    ResponsePlan::map_ok

    fn[A, B, E, M] ResponsePlan::map_ok(self : ResponsePlan[Result[A, E], M], f : (A) -> B raise) -> ResponsePlan[Result[B, E], M]

    Transforms successful Result values while retaining their error type.

    ResponsePlan::map_with_head

    fn[A, B, M] ResponsePlan::map_with_head(self : ResponsePlan[A, M], f : (A,
    ResponseHead
    ) -> B raise) -> ResponsePlan[B, M]

    Transforms an interpreted value together with actual response metadata.

    ResponsePlan::new

    fn[T] ResponsePlan::new(decode : (
    ResponseHead
    , Bytes) -> T raise) -> ResponsePlan[T, Buffered]

    Buffers any status within max_body_bytes, then invokes a custom decoder. The decoder sees immutable bytes, never ownership of the original stream.

    ResponsePlan::prepared_stream

    fn[A, T] ResponsePlan::prepared_stream(decode : (
    ResponseHead
    , Bytes) -> Result[A, T] raise, use_ : async (A) -> T) -> ResponsePlan[T, Streaming]

    Decodes bounded bytes inside the attempt, then hands a successful value to a scoped callback after release and retry decisions. An Err is already the final value, so failures can be inspected before any callback starts.

    ResponsePlan::reject

    Rejects a head-selected representation with a bounded diagnostic excerpt. This description always returns UnexpectedStatus, including for HTTP 2xx.

    ResponsePlan::stream

    Creates a scoped stream interpreter that also receives the effective max_body_bytes, for a consumer that buffers the stream itself and must keep the decoded-byte limit the Client would have applied. Client owns cleanup and never retries once this interpreter starts.

    RetryContext

    pub(all) struct RetryContext {
    request :
    RequestHead

    idempotent : Bool
    bucket : String
    attempt : Int
    random : Double
    now_unix_ms : Int64
    remaining_ms : Int64?
    replayable : Bool
    default_decision :
    Decision

    }

    Inputs to a retry decision. Attempt zero is the initial exchange.

    RetryContext::capture

    fn RetryContext::capture(request :
    Request
    , attempt? : Int, bucket? : String, random? : Double, now_unix_ms? : Int64, remaining_ms? : Int64, replayable? : Bool) -> RetryContext

    Captures live request facts at the boundary before invoking a policy. Later acquisition or closure of the body cannot change this snapshot.

    RetryContext::new

    fn RetryContext::new(request :
    RequestHead
    , idempotent~ : Bool, replayable~ : Bool, attempt? : Int, bucket? : String, random? : Double, now_unix_ms? : Int64, remaining_ms? : Int64, default_decision? :
    Decision
    ) -> RetryContext

    Creates a context, with midpoint jitter and no deadline by default.

    RetryPolicy

    A retry policy for Client: a RetryRule of gaato/mttp/client/retry applied to HTTP outcome snapshots.

    Start from a preset — conservative() (the default), none() — or from any rule with from_rule, and adjust it with with_max_retries, allow_non_idempotent or a struct update of rule.

    RetryPolicy::allow_non_idempotent

    fn RetryPolicy::allow_non_idempotent(self : RetryPolicy) -> RetryPolicy

    This policy with every Idempotent method set widened to Any, in its status rules and for failures after sending: what retry_non_idempotent:true did before 0.7.0. A retried POST may then run twice.

    RetryPolicy::conservative

    fn RetryPolicy::conservative() -> RetryPolicy

    An opt-in conservative policy: two retries; 429 and 408, 409, 500, 502, 503 and 504 only for an idempotent request (@http.is_idempotent, using the method or explicit replay_safe); a failure that proves nothing was sent (TransportError::is_unsent) for any method, and a proxy's refusal only when its status is one of those; a timeout or network failure after sending only for an idempotent request.

    The wait is retry-after-ms, else Retry-After (seconds or an HTTP date), and a server that asks for more than 60 s is not retried; Retry-After: 0 retries at once. Without one, the backoff starts at 500 ms, doubles up to 30 s and is spread by ±20 %.

    RetryPolicy::decide

    Applies pure HTTP retry data to a snapshot.

    RetryPolicy::default

    fn RetryPolicy::default() -> RetryPolicy

    conservative().

    RetryPolicy::equal

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

    Equality for retry policy values.

    RetryPolicy::from_rule

    A policy applying rule as it is.

    RetryPolicy::none

    fn RetryPolicy::none() -> RetryPolicy

    conservative() without retries.

    RetryPolicy::not_equal

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

    Equality for retry policy values.

    RetryPolicy::to_repr

    Debug representation of retry policy values.

    RetryPolicy::with_max_retries

    fn RetryPolicy::with_max_retries(self : RetryPolicy, max_retries : Int) -> RetryPolicy

    This policy with max_retries retries after the first attempt.

    Streaming

    pub(all) struct Streaming {
    }

    Response processing which may hand a live body to a consumer.

    Streaming::fresh

    #deprecated("`Streaming::fresh` is deprecated, use `ResponseMode::fresh` instead.")
    fn Streaming::fresh() -> Streaming

    Keeps the trait's operations from being promoted to methods of the marker.

    Streaming::may_stream

    #deprecated("`Streaming::may_stream` is deprecated, use `ResponseMode::may_stream` instead.")
    fn Streaming::may_stream(_ : Streaming) -> Bool

    Keeps the trait's operations from being promoted to methods of the marker.

    as_both

    fn[A, B] as_both(left : ResponsePlan[A, Buffered], right : ResponsePlan[B, Buffered]) -> ResponsePlan[(A, B), Buffered]

    Buffers once within max_body_bytes, then reads the same immutable bytes with both interpretations, left first. Streaming readers are excluded by the argument types. Selectors run once each; rejected bodies stay bounded.

    as_bytes

    fn as_bytes() -> ResponsePlan[Result[Bytes, ResponseFailure], Buffered]

    Buffers successful bodies; non-2xx responses are bounded failure values.

    as_form

    fn as_form(max_pairs? : Int) -> ResponsePlan[Result[Array[(String, String)], ResponseFailure], Buffered]

    Buffers and decodes form pairs with the model's forgiving UTF-8 and percent decoding. Both max_body_bytes and max_pairs apply.

    as_json

    Parses and decodes JSON without raising received-response failures.

    as_json_value

    Parses JSON; status and decoding failures are received-response values.

    as_lines

    fn as_lines(receive : async (String) -> Unit, max_line_bytes? : Int) -> ResponsePlan[Result[Unit, ResponseFailure], Streaming]

    Streams LF-separated UTF-8 lines, stripping CR in CRLF and replacing malformed UTF-8. A final nonempty unterminated line is delivered; a lone CR is data. Limits content bytes per line (default one MiB), retaining at most that limit plus a pending CR. Callback errors close the body and never retry an already-delivered stream.

    as_sse

    fn as_sse(receive : async (
    SseEvent
    ) -> Unit, max_event_bytes? : Int, stop? : (
    SseEvent
    ) -> Bool) -> ResponsePlan[Result[Unit, ResponseFailure], Streaming]

    Reads bounded SSE events until EOF or a protocol stop predicate matches. An event over max_event_bytes raises TooLarge(limit) in phase Body as soon as the chunk holding it is read, after the events before it in that chunk were delivered.

    as_stream

    Hands a successful stream to a callback once, closing it on every exit. Rejected responses are buffered failure values before any handoff.

    as_string

    fn as_string() -> ResponsePlan[Result[String, ResponseFailure], Buffered]

    Decodes UTF-8 lossily while preserving HTTP failures as values.

    final_url

    let final_url :
    AttributeKey
    [String]

    Final URL sent by FollowRedirects, including when no hop was followed.

    from_fn

    Wraps a function as a backend. The wrapper refuses an invalid request or invalid options before calling send (Request::validate, a stream body declaring a negative length, SendOptions::validate; all unsent), refuses sends after close (TransportError(Closed, Pool)), and calls on_close at most once. send receives the options unchanged and owns the rest of the Backend contract: it closes a request stream it takes and leaves the response body to the caller.

    from_handler

    Runs an in-process handler as a backend, through from_fn. The handler does no waiting on SendOptions; a Client with a Timer still bounds each attempt with its own deadline.

    from_head

    fn[T, M : ResponseMode] from_head(choose : (
    ResponseHead
    ) -> ResponsePlan[T, M]) -> ResponsePlan[T, M]

    Selects once per head, preserving the selected branch's actual lifetime.

    get

    fn get(url : String) -> RequestPlan[Result[String, ResponseFailure], Buffered]

    Creates a GET request with the default Result-valued string reader.

    global_exempt

    let global_exempt :
    AttributeKey
    [Bool]

    Whether the request bypasses global admission limits.

    ignore_body

    fn ignore_body() -> ResponsePlan[Result[Unit, ResponseFailure], Buffered]

    Discards a successful body; rejected statuses retain a bounded excerpt.

    max_error_body

    let max_error_body : Int

    Maximum retained bytes of a rejected or undecodable body: one MiB.

    operation_id

    let operation_id :
    AttributeKey
    [String]

    The endpoint operation identifier, independent of admission buckets.
    fn parse_link_next(headers :
    Headers
    ) -> String?

    Returns the target of the first RFC 8288 Link value whose relation, given by its first rel parameter, includes next, across repeated headers and comma-separated link values.

    parse_retry_after

    fn parse_retry_after(value : String, now_unix_ms? : Int64) -> Int?

    Parses nonnegative seconds (including decimals) or, given a clock, an HTTP-date in any of RFC 9110's three forms. Results are truncated to milliseconds and saturated at Int max.

    test {
    assert_eq(@retry.parse_retry_after("1.2349"), Some(1234))
    }

    post

    fn post(url : String) -> RequestPlan[Result[String, ResponseFailure], Buffered]

    Creates a POST request with the default Result-valued string reader.

    rate_limit_bucket

    let rate_limit_bucket :
    AttributeKey
    [String]

    An explicit local admission bucket; absent metadata selects "default".

    read_body

    async fn read_body(body :
    Body
    , limit : Int) -> Bytes raise
    TransportError

    Buffers a body within the supplied byte limit and closes its stream.

    redirect_history

    Followed hops, at most the effective redirect limit.

    redirect_limit

    let redirect_limit :
    AttributeKey
    [Int]

    How many redirect hops this request may follow, overriding the configuration of a redirect-following wrapper (RedirectConfig.max_redirects of FollowRedirects), which reads it from the request's attributes. 0 (or less) follows no redirect and returns the 3xx response as it is. RequestPlan::follow_redirects and max_redirects set it.

    request

    Pairs a wire request with a UTF-8 string reader accepting successful statuses.

    retry_after_ms

    fn retry_after_ms(headers :
    Headers
    , now_unix_ms? : Int64) -> Int?

    Prefers the first retry-after-ms value; falls back to retry-after when that header is absent or unparseable, as the official OpenAI SDKs do.

    status_of

    fn status_of(error : ResponseFailure) -> Int?

    Returns Some of the received HTTP status for a response failure.