errand 0.1.2

Async small-web (smolweb) transport: gemini, gopher, finger, spartan, nex, guppy, and titan in one scheme-routed fetch, plus titan-upload and misfin-send write companions. Bytes in, bytes out, no HTTP, host-agnostic.
Documentation

errand

Async smolweb transport in one scheme-routed call.

errand fetches a URL over a small-web protocol and hands back the raw bytes, a normalized status, and a MIME hint. One call, routed by scheme:

let page = errand::fetch("gemini://geminiprotocol.net/").await?;
if page.status == errand::Status::Success {
    println!("{} bytes of {}", page.body.len(), page.mime().unwrap_or("?"));
}

It does not speak HTTP, on purpose. HTTP is already well served by reqwest, and a browser-extension host gets HTTP from the browser. errand covers the gap they leave: the protocols of the small web, with a small dependency cone.

Made with AI

Protocols

errand::fetch routes by URL scheme and returns a normalized Status, the protocol meta line, and the raw body. It does not follow redirects; the caller decides. Seven read schemes are routed through fetch:

Scheme Port Transport Notes
gemini:// 1965 TLS (TOFU) self-signed capsules, per-host pinning
gopher:// 70 plaintext TCP no status code
finger:// 79 plaintext TCP no status code
spartan:// 300 plaintext TCP numeric status
nex:// 1900 plaintext TCP no status code
guppy:// 6775 UDP (stop-and-wait) numeric status, ACK-per-packet
titan:// 1965 TLS (TOFU) gemini's upload sibling; see below

Write companions

Two schemes write rather than fetch, so they are direct calls, not part of fetch:

  • titan_upload — Titan (titan://, port 1965) is gemini's upload sibling. Call it with the body bytes, MIME type, and an optional token. A bare titan:// URL passed to fetch sends a zero-byte upload and returns the server's gemini-format response (typically a redirect to the read location).
  • misfin_send — Misfin (misfin://, port 1958) is gemini-style peer-to-peer mail. Delivery opens a TLS connection presenting a caller-supplied client certificate (ClientIdentity) and writes a misfin://<mailbox>@<host> <message> request line. errand owns only the client send side; it does not generate, store, or rotate certificates, and it does not serve a mailbox.

API

  • fetch(url: &str) / fetch_url(url: &Url) — fetch over the URL's scheme.
  • fetch_timeout(url, Duration) / fetch_url_timeout(url, Duration) — the same with a per-request timeout; returns Error::Timeout if it does not complete.
  • titan_upload(...) — Titan write.
  • misfin_send(...) with ClientIdentity and MISFIN_PORT — Misfin mail send.
  • set_trust_store, TofuStore, InMemoryTofu, PermissiveTofu — TOFU trust policy (see below).
  • Scheme — the routable scheme enum, with Scheme::parse(&str) and default_port().
  • Response { url, status, raw_status, meta, body } with .mime().
  • StatusSuccess, Input, Redirect, Failure, CertRequired.
  • ErrorUnsupportedScheme, BadUrl, Connect, Io, Protocol, Timeout, CertificateChanged { host, pinned, seen }.
  • Url re-exported from the url crate.

Response::raw_status preserves the protocol's own two-digit code for the schemes that have one (gemini, spartan, guppy, titan); it is None for gopher, finger, and nex, which carry no status.

Trust (TOFU)

Gemini capsules are conventionally self-signed, so there is no CA to anchor trust. errand pins the SHA-256 of a host's leaf certificate on first contact and requires every later visit to present the same one. A changed certificate (a man-in-the-middle, a key rotation, or a moved host) surfaces as Error::CertificateChanged and the request is not sent; the embedder decides whether to re-pin.

The pin store is the TofuStore trait, so the embedder chooses durability. InMemoryTofu holds pins for the process lifetime. A host with a profile can supply its own durable store. The store is installed once via set_trust_store; until then errand uses PermissiveTofu (accept-any), so the module changes nothing for callers that do not opt in.

Install

[dependencies]
errand = "0.1"

Or as a git dependency:

[dependencies]
errand = { git = "https://github.com/mark-ik/errand" }

Build and test

cargo build
cargo test

Tests are inline #[cfg(test)] modules (request construction, scheme routing, status mapping). There is no tests/ integration directory and no examples/.

Dependencies and platform

  • url 2.5
  • tokio 1 (features net, io-util, time; no full runtime)
  • rustls 0.23 and tokio-rustls 0.26 on the ring provider, with tls12
  • ring 0.17 for leaf-certificate SHA-256 (TOFU pinning)

aws-lc-rs is deliberately left off so the crate builds without a C toolchain. Edition 2021, MSRV (rust-version) 1.74.

Status

Version 0.1.0. Single crate, no workspace. Consumed one-way as a git dependency by the broader Mere stack: the fetch actor routes http(s) to netfetcher and the smolweb schemes here to errand, and the Misfin mail crate uses errand as its client send transport. Dependency direction is one-way (consumers pull errand, never the reverse).

License

MPL-2.0. See LICENSE.