// The transport seam. A Backend moves one exchange and nothing more: the
// connection engines of gaato/http, http-async's runtime backends, and the
// wrappers and stubs of gaato/mttp all implement it, so a client policy is
// written once against this trait. CONTRACT.md (Backend; SendOptions and
// BodyTiming) states what each side may rely on.
///|
/// 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)`.
pub(open) trait Backend {
/// Sends one exchange under `options`.
async fn send(Self, Request, SendOptions) -> Response raise TransportError
/// Releases the backend's resources. Idempotent; the default does nothing.
fn close(Self) -> Unit = _
}
///|
impl Backend with fn close(_) {
()
}
///|
/// How the caller will consume the response body, which decides what the
/// attempt deadline covers and how many bytes the backend admits.
///
/// ```mbt check
/// 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))
/// }
/// ```
#derive(Eq, Debug)
#non_exhaustive
pub(all) enum BodyTiming {
/// The caller reads the whole body inside the attempt: `timeout_ms` covers
/// the head and the body, and at most `max_bytes` body bytes are admitted
/// from the wire (after transfer framing, before content decoding). The
/// read that would cross the limit raises `TooLarge(limit)` in phase Body.
Exchange(max_bytes~ : Int32)
/// The caller streams the body: `timeout_ms` ends when the head arrives,
/// and each later read waits at most `idle_ms` (`None`: no idle limit).
/// The total length is the reader's to bound.
Stream(idle_ms~ : Int32?)
}
///|
/// Compares body timings.
pub extend BodyTiming with Eq::{equal, not_equal}
///|
/// Formats a body timing for diagnostics.
pub extend BodyTiming with @debug.Debug::{to_repr}
///|
/// The per-send settings a caller hands a `Backend`. Build them from
/// `SendOptions::default()` with a record update; the field set is frozen.
#derive(Eq, Debug)
pub(all) struct SendOptions {
/// The attempt deadline in milliseconds, or `None` for none at this layer.
timeout_ms : Int32?
/// What the deadline covers and how many body bytes are admitted.
body : BodyTiming
}
///|
/// Compares send options.
pub extend SendOptions with Eq::{equal, not_equal}
///|
/// Formats send options for diagnostics.
pub extend SendOptions with @debug.Debug::{to_repr}
///|
/// No attempt deadline and a buffered body of at most 64 MiB on the wire.
pub fn SendOptions::default() -> SendOptions {
{ timeout_ms: None, body: Exchange(max_bytes=64 * 1024 * 1024), }
}
///|
/// 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.
///
/// ```mbt check
/// 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")
/// }
/// }
/// ```
pub fn SendOptions::validate(self : SendOptions) -> Unit raise TransportError {
if self.timeout_ms is Some(ms) && ms <= 0 {
raise TransportError(
kind=Timeout(ms~),
phase=Pool,
detail="the attempt deadline is not positive",
)
}
match self.body {
Exchange(max_bytes~) if max_bytes < 0 =>
raise TransportError(
kind=Invalid,
phase=Pool,
detail="negative body byte limit",
)
Stream(idle_ms=Some(ms)) if ms <= 0 =>
raise TransportError(
kind=Invalid,
phase=Pool,
detail="the idle read limit is not positive",
)
_ => ()
}
}