Header
struct HeaderOne Name: Value pair. Names are compared case-insensitively.
Implements Copyable, Movable, Writable
- Fields
name: Stringvalue: String
write_to
def write_to(self, mut writer: T) objectstore.httpobjectstore.http — an HTTP/HTTPS client over libcurl.
Why libcurl. Nothing in the Mojo package ecosystem gives this repo a TLS
capable HTTP client: floki, flare, lightbug-http and fire-http all fail
to resolve from conda ("No candidates were found") on both the stable and the
nightly toolchain, and an object store without HTTPS is not an object store.
conda-forge's libcurl ships TLS and a CA bundle for every platform we target,
so this tin follows the magmalake FFI recipe (zstd.mojo, lz4.mojo): a tiny C
shim built as a pixi-build-cmake package into $CONDA_PREFIX/lib, found by
_dlopen.open_library (with or without CONDA_PREFIX) and dlopened once per
process through an OwnedDLHandle.
The shim exists because curl_easy_setopt is a C variadic, which a dlopened
symbol cannot express; shim/curl_wrapper.c collapses a whole request into one
fixed-arity call and hands back an opaque result object read through accessors.
Every FFI-calling worker takes the handle as a borrowed imm parameter: Mojo
destroys a value at its last syntactic use, so a function that both owned the
handle and called get_function on it would dlclose the library before the
returned function pointer is invoked.
Connection reuse. The curl easy handle lives in the shim and persists
across requests, so a second request to the same host reuses its TCP and TLS
session instead of shaking hands again — the difference between ~2.9 ms and
~0.2 ms per range read on loopback. Requests go to a process-wide pool by
default; new_connection_pool hands out a private one for a caller that wants
its connections kept apart.
Threads. Requests can be made from several threads at once (shim ABI
5): SHARED_POOL and a batch's multi handle are per thread in the shim, with
their own connections, and what threads share — DNS and TLS sessions — sits
behind locks.
A private pool from new_connection_pool belongs to one thread at a time.
Batches. request_many sends several requests at once and returns when
all have answered: one thread, curl's multi interface underneath, its own
persistent connections. A ranged read of a Parquet file is mostly waiting for
first bytes, so overlapping the waits is most of the speed there is to have.
Retries. Transport failures, 429 and most 5xx are retried with exponential
backoff and jitter, but only for requests that can safely be repeated — see
RetryPolicy. A response that is returned is never retried behind the
caller's back on a method the server might have already acted on.
Headerstruct HeaderOne Name: Value pair. Names are compared case-insensitively.
Implements Copyable, Movable, Writable
name: Stringvalue: Stringwrite_todef write_to(self, mut writer: T) RetryPolicystruct RetryPolicyWhen to try again, and how long to wait first.
Defaults: three retries (four attempts), 100 ms doubling to a 20 s ceiling,
and idempotent requests only. POST is retried only when the caller
has said the server can deduplicate it by supplying an Idempotency-Key
header — which is exactly what an Iceberg REST commit does, and why the
header is the trigger rather than a flag on the client.
Implements Copyable, Movable
max_retries: Int: Attempts after the first. 0 disables retrying entirely.base_delay_ms: Intmax_delay_ms: Intretry_non_idempotent: Bool: Escape hatch for a caller that knows its POST is safe to repeat and cannot add a header. Off, because guessing wrong duplicates a write.__init__def __init__(out self) nonedef none() -> Self One request, one answer — the 0.1.x behaviour.
Responsestruct ResponseA completed HTTP response: status line, headers, and the whole body.
Implements Copyable, Movable
status: Intheaders: List[Header]body: List[UInt8]__init__def __init__(out self, status: Int, var headers: List[Header], var body: List[UInt8]) okdef ok(self) -> Bool headerdef header(self, name: String) -> String The first value for name (case-insensitive), or "".
has_headerdef has_header(self, name: String) -> Bool textdef text(self) -> String body_excerptdef body_excerpt(self, limit: Int = Int(256)) -> String The first limit bytes of the body, for error messages.
S3 and Iceberg REST both answer errors with a short XML or JSON
document; truncating keeps a raised Error readable when the body is
a 20 MB object instead.
raise_for_statusdef raise_for_status(self, what: String) BatchRequeststruct BatchRequestOne request of a batch: a GET or HEAD, optionally ranged.
expect_len, when known, sizes the response buffer up front.
Implements Copyable, Movable
method: Stringurl: Stringheaders: List[Header]range_start: Intrange_end: Intexpect_len: Intgetdef get(var url: String, var headers: List[Header] = List(), range_start: Int = Int(-1), range_end: Int = Int(-1)) -> Self HttpClientstruct HttpClientAn HTTP client over a persistent, reused connection.
The client itself holds no socket: pool names a curl handle living in
the shim, and SHARED_POOL — the default — is the process-wide one. Two
HttpClient values therefore share connections, which is what makes an
Iceberg scan that opens a new S3Client per file cheap. With the default
pool, threads may each make requests at once.
Redirects are off by default: a presigned S3 URL carries its signature in the query string and must never be silently re-issued elsewhere.
Implements Copyable, Movable
timeout_ms: Intfollow_redirects: Boolverify_tls: Boolpool: Int: SHARED_POOL, or a handle from new_connection_pool() that the caller is responsible for freeing.retry: RetryPolicy__init__def __init__(out self) requestdef request(self, method: String, url: String, headers: List[Header] = List(), body: Span[UInt8] = Span(), range_start: Int = Int(-1), range_end: Int = Int(-1)) -> Response Performs the request, retrying per self.retry.
Raises only on transport failure — a 404 or a 500 comes back as a
Response so the caller can decide. A retryable status that survives
the last attempt is returned too: the caller sees the server's own
answer rather than an invented error.
request_manydef request_many(self, reqs: List[BatchRequest], max_parallel: Int = Int(16)) -> List[Response] Every request in reqs at once, answers in the same order.
Like request, a status comes back as a Response and only a
transport failure raises. A request that failed in transport or got
a retryable answer is tried again on its own, under self.retry.
Batches are GETs and HEADs, which are always safe to repeat. A
shim older than ABI 4 has no batches: then they run one by one.
getdef get(self, url: String, headers: List[Header] = List(), range_start: Int = Int(-1), range_end: Int = Int(-1)) -> Response headdef head(self, url: String, headers: List[Header] = List()) -> Response putdef put(self, url: String, body: Span[UInt8], headers: List[Header] = List()) -> Response postdef post(self, url: String, body: Span[UInt8], headers: List[Header] = List()) -> Response deletedef delete(self, url: String, headers: List[Header] = List()) -> Response new_connection_pooldef new_connection_pool() -> Int A private pool: one persistent curl handle, its own connections.
Returns an opaque handle to pass as HttpClient.pool. The caller owns
it and must call free_connection_pool; there is no destructor here
because HttpClient is Copyable, and a copyable owner of a C resource
is a double free waiting to happen. Most callers want SHARED_POOL.
free_connection_pooldef free_connection_pool(pool: Int) Closes a pool from new_connection_pool and its live connections.
pool_statsdef pool_stats(pool: Int = Int(0)) -> Tuple[Int, Int] (requests, connections) for a pool since the last reset_pool_stats.
connections is what curl actually opened, so reuse is the observation
that it stays at 1 while requests climbs — which is how the tests prove
it rather than inferring it from a stopwatch.
reset_pool_statsdef reset_pool_stats(pool: Int = Int(0)) shim_abidef shim_abi() -> Int 1 = a fresh curl handle per request (0.1.x); 2 = pooled connections; 3 = instance_credentials_blob; 4 = batches (request_many); 5 = safe from several threads.
instance_credentials_blobdef instance_credentials_blob() -> String The EC2 instance role's credentials, as the shim's IMDSv2 lookup hands them back: access-key-id secret token expiry-epoch, or "" when there are none.
The shim caches the answer for the process, "none" included, so this is
cheap to call per request. A shim older than ABI 3 has no lookup and
answers "" as well.
header_blobdef header_blob(headers: List[Header]) -> String Headers as the shim wants them: Name: Value lines joined by LF.
An HTTP header value cannot contain a bare LF, so this encoding is
lossless and spares the FFI boundary a char*[].
is_idempotentdef is_idempotent(method: String, headers: List[Header]) -> Bool RFC 9110 idempotency, plus the Idempotency-Key convention.
GET, HEAD, PUT, DELETE, OPTIONS and TRACE are idempotent by
definition: repeating one cannot commit a second effect. POST and
PATCH are not, so they are only repeatable when the request carries a
key the server can deduplicate on.
retryable_statusdef retryable_status(status: Int) -> Bool 429 and 5xx, except the two that mean "never, no matter how often".
501 Not Implemented and 505 HTTP Version Not Supported are permanent facts about the server; retrying them is pure latency.
s3_retryable_errordef s3_retryable_error(body: Span[UInt8]) -> Bool S3's two "come back later" errors, which arrive with unhelpful statuses.
SlowDown is throttling and RequestTimeout means the server gave up
waiting on the body; AWS returns the latter as a 400, so status alone
would call it permanent. Only the first bytes are inspected, and only for
a response that already failed, so this never touches an object body.
backoff_delay_msdef backoff_delay_ms(policy: RetryPolicy, attempt: Int) -> Int Exponential backoff with jitter: half the window fixed, half random.
attempt counts from 1. Full jitter would sometimes retry instantly and
hammer a server that is already asking for room; half is the usual
compromise. The jitter source is the monotonic clock's low bits — there is
no RNG in reach here, and a retry schedule does not need a good one.
parse_headersdef parse_headers(raw: String) -> List[Header] Parses curl's raw header text into pairs.
curl emits one header block per response it sees, so a redirect chain or a
100 Continue produces several. Only the last block describes the
response the caller gets, so a new status line resets the accumulator.
batch_statsdef batch_stats() -> Tuple[Int, Int] (requests, connections) for batches since reset_batch_stats: the connections stay few while requests climb, because batches reuse them.
reset_batch_statsdef reset_batch_stats() curl_versiondef curl_version() -> String Libcurl's own version banner, e.g. libcurl/8.9.1 OpenSSL/3.3.1 ….
comptime DEFAULT_MAX_PARALLEL = 16Requests a batch keeps in flight at once, on as many connections. The Rust Iceberg reader keeps 10 range reads in flight per file; S3 serves each connection at roughly 100 MB/s, so 16 covers a node's share of bandwidth without opening sockets by the hundred.
comptime DEFAULT_TIMEOUT_MS = 30000Total request deadline. Range reads of a Parquet footer are small; a 20 MB object over a slow link is not, so this is generous rather than tight.
comptime SHARED_POOL = 0The process-wide connection pool, which is what every HttpClient uses unless it is given one of its own.
comptime S3_RETRYABLE_CODES = 2