base-http-client 0.1.0

Supervised pooled HTTP clients for Base scheduler capabilities
Documentation

Base HTTP Client

An unpublished endpoint-scoped HTTP client, inspired by Socketry's Async::HTTP::Client. It uses the base-pool 0.3 multiplex pool with capacity one for HTTP/1 connections. The API is experimental.

Client::new(&scheduler, Endpoint::new(address), options) returns a request handle and an explicit Driver. Poll driver.run(&lifetime_cancellation) concurrently with calls and application body consumers. The connection retains transport directions, framing state and reusable codec buffers. The driver supervises dialing, queued calls, TLS progress and retirement. The client provides pull-body call and direct writable open operations; clones share one endpoint pool.

call returns as soon as final response metadata is available. Request upload continues when response consumption pauses or finishes. Incoming frames use a bounded queue. A pool use stays charged until upload finishes and the response is consumed; dropping/closing an unfinished response cancels the exchange and retires its HTTP/1 connection. Pool resource capacity remains charged through both lane retirements and exclusive socket closure. No unread response is automatically drained.

begin_close rejects new submissions and lets accepted calls finish, then retires the idle connections. Cancelling the driver's lifetime signal terminates outstanding calls and still joins cleanup. Dropping the final client handle begins graceful closure. Dropping the driver is synchronous abandonment and does not perform an asynchronous join; always await its run future. Application errors should cancel its lifetime signal before joining, especially if an upload producer is still waiting.

The existing pull-body call path uses statically dispatched transport operations. Heterogeneous request bodies cross its queue through one boxed producer stream per call, with dynamic polling per frame and no boxed future allocation per read. Data is normalized to Vec<u8>: vectors move, while borrowed slices are copied. This boundary supports one durable client accepting different body types; it is independent of the scheduler's precise I/O-handle lifetime bounds. Bodies and interim observers may borrow for the driver scope. Spawning the driver requires its captured work to be Send + 'static. Forwarding a transient borrowed server input through a longer-lived client needs an additional ownership design; the existing low-level proxy remains available.

Establishment uses base-endpoint::AddressEndpoint over a resolved TCP address. Enable the rustls feature and use Endpoint::with_tls(config, server_name) for HTTPS with explicit trust and identity. Options::protocols supplies ALPN offers and selects the HTTP engine through base-http-protocol, before pool publication. The initial engine is HTTP/1; missing ALPN is compatible by default and can be rejected with require_alpn. Endpoint authority supplies missing request authority independently of the routing address and authenticated server name. DNS integration, HTTP/2/3, automatic retry, redirect and proxy policy remain follow-up work. Managed high-level client upgrades and CONNECT are rejected before dialing. Expect: 100-continue does not delay upload; call_with_interim observes informational responses without accumulating them.

Options bounds pool resources/builds/waiters, total accepted calls and response queue bytes/frames. A saturated accepted-call limit returns Error::Busy. Queue byte capacity must accommodate one maximum parser read or trailer section. A caller cancellation signal remains associated with the exchange after call returns; cancelling an individual queued body read leaves that body resumable. Direct input cancellation has the stricter behavior described below.

Run the complete local example with cargo run --example client_server, or cargo run --features rustls --example client_server -- --https for HTTPS. The TLS demo trusts its explicitly generated local identity. It sends three POSTs through one pooled HTTP/1 session to an actual base-http-server echo handler: a buffered call, a sequential writable upload and an independently driven split upload/response. It then joins both peers and cleanup. Rust 1.87 is the minimum supported compiler.

TCP integration coverage exercises reuse, early final responses, independent upload under response backpressure, interims and trailers, partial response abandonment, cancelled/dropped calls, cancelled pool waiters, admission limits and supervised shutdown. The public semantic, HTTP/1 and server suites are also checked on Rust 1.87.

The HTTPS integration tests cover TLS 1.2/1.3, a stalled handshake alongside an active client, independent HTTP peers, strict/compatible missing-ALPN behavior, failed identity verification and upload under response backpressure. TLS reads, writes and flushes poll retained I/O futures directly. The connection future supervises control output and joined retirement; no transport task is detached.

Direct writable requests

An active WritableRequest is distinct from a semantic Request<B>. It holds the admitted connection's output direction and a pending response. The sequential form is:

let mut request = client.open(Method::POST, "/upload", &cancel).await?;
request.write(b"Hello ", &cancel).await?;
request.write(b"World", &cancel).await?;
let mut response = request.finish(&cancel).await?;

open_request(metadata, cancellation) allows fields, authority and a known content_length. Unknown length uses HTTP/1.1 chunked framing. Writes directly drive the encoder and flush accepted bytes to the transport; there is no HTTP body-data channel. Vectors move into the encoder, while slices copy into owned vectors. One boxed sink retains output operation state and one boxed stream retains the response decoder across reads; neither creates a boxed future per frame. Transport calls remain statically dispatched. Established requests acquire these retained directions directly in the calling task; ordinary open/write/read completion does not dispatch to the client driver. Short locks transfer ownership and are never held across I/O.

finish completes output before reading the final response metadata. For streaming echo, early responses, Expect/Continue coordination or a proxy, use request.split() to obtain a RequestBodyWriter and a PendingResponse future. Poll upload and response consumption concurrently, through joined futures or independently supervised application tasks. Response body reads drive the HTTP decoder directly. Successful response EOF does not finish an active upload; both directions must complete before connection reuse. The split does not spawn tasks. Proxy forwarding must use duplex progress by default; it must not infer that sequential forwarding is safe from the method or body length. The complete example shows both forms.

PendingResponse polls the existing response stream directly and transfers it to the returned body; it does not allocate another boxed future. Each exchange has one shared completion record containing admission, directional completion, output closure mode and a deferred-close result/waker. Direction guards keep their ordinary completion mode inline. Results survive cancelled or dropped flush waits. Completion records are distinct for successive requests, so old handles cannot cancel a later exchange on a reused connection. Codec owners return their directions before releasing admission; lifecycle notifications and completion wakes run outside the completion-state mutex. See the exchange allocation investigation.

Each established connection reuses empty pinned storage for the opening future, request sink and response stream. A live operation exclusively owns its allocation; dropping its handle clears all captures in place before caching storage. Caches retain at most one empty allocation of each type and have weak return links. They never retain exchange admission or reset a completed request's cancellation state. Known-empty responses release their input adapter immediately, even while the response value remains alive. This trades bounded idle storage and small factory/cache bookkeeping for lower steady-state allocation traffic; fresh connection construction still allocates. See the storage reuse measurements.

Normal direct completion wakes supervision only when the client is closing and its final accepted exchange has drained. Pool retirement notifications are independent of normal admission and reuse in the local pool optimization. Cancellation wrappers check the signal before polling work, register cancellation only after work returns Pending, and recheck the signal while registering. Their concrete pinned state stores the nested future directly. This keeps already-cancelled operations from starting and preserves cancellation wakeups without subscribing immediately ready work.

The private sequential request sink uses pinned replacement when transferring its codec into and out of a write future. The storage caches use the same mechanism for initialization and clearing. This avoids Rust 1.87 copies of inactive future storage while preserving in-place destruction and one owner per direction. No new public body or driver interface is introduced. See the orchestration investigation for source, assembly and measurements.

open_request_with_interim accepts an informational-response observer; it runs when the pending response is polled. Merely opening or writing the request does not read interims or wait for 100 Continue. Output accepts ordinary data and terminal frames, including trailing fields. Synchronous close_write(None) hands final framing back to the supervisor; flush or finish observes its completion. Dropping unfinished output, a pending response or partial response input abandons the exchange and retires HTTP/1. A dropped pending write is not replayed. Pre-cancelled operations leave the direction usable; cancellation during a direct input read conservatively retires the exchange. Dropping a pending read future retains its pinned decoder state for a subsequent read.

Poll the driver for connection establishment, TLS progress, queued call, deferred synchronous close_write and retirement. Established plaintext open/write/read requests can complete repeatedly without polling it. Encrypted reads, writes and flushes also make direct progress. Incoming TLS records are processed on application reads; control output and cleanup remain supervised. Regressions freeze supervision and exercise both directions for TLS 1.2/1.3. During cancellation/shutdown, poll or drop all outstanding handles before awaiting driver completion: the supervisor cannot reclaim a codec still owned by the application. Pool uses and retiring resources remain charged until both lanes return and cleanup completes. call and open share admission, request limits, TLS/ALPN and the same pool; no automatic replay is introduced. Returning both codecs synchronously releases the pool use before the next direct request; there is no supervisor acknowledgment on the normal completion path.

Connection task placement

Direct HTTP/1 exchanges execute on their application tasks, with connection-owned state and no mandatory per-request worker handoff. The current scoped prototype still polls supervised connection work together inside the client-wide driver. Queued calls and TLS progress therefore do not distribute across scheduler workers merely by spawning that one future.

The scalable design separates client supervision from independently scheduled connection drivers. An HTTP/2 connection driver owns framing, control traffic and stream demultiplexing; the scheduler can distribute separate connections across workers. TLS progress belongs with its connection. The supervisor retains task handles, cancellation and pool retirement accounting and joins them at shutdown. Connections do not each need a dedicated thread, and HTTP/1 direct bodies still do not require pumping tasks.

The scheduler's current Spawn accepts Send + 'static futures. The borrowed scoped client cannot simply submit its child futures to it. A runtime-owned execution mode needs owned scheduler/transport state and owned task captures, or an explicit scoped executor contract. No unsafe lifetime extension or global scheduler is implied. Per-connection task placement remains required before scaling the client to HTTP/2 workloads; it is not implemented by this HTTP/1 experiment.

The proxy integration experiment uses the direct upstream client with borrowed downstream HTTP/1 input and always joins upload/download. Independent peers verify an 8 MiB upload while downstream response reads pause, and upload continuing after an early complete response, followed by upstream connection reuse. This exercises the ownership/progress pattern; it does not add a production proxy handler or policy API.