-
Notifications
You must be signed in to change notification settings - Fork 1
HTTP Client
The client shares bounded pools per authority. HTTP/2 requests are multiplexed by a dedicated driver per connection and assigned by current dispatch reservations; max_connections_per_host controls how many independent drivers may be opened under contention. This is connection-aware concurrency, not a promise that one HTTP/2 connection scales linearly with caller threads.
use courierust::courierust_client::{Client, ClientConfig};
use courierust::courierust_http::header::HeaderMap;
use std::time::Duration;
let cfg = ClientConfig {
// Prefer HTTP/2 (h2c prior knowledge). HTTP/1.1 is used when false.
http2: true,
// Max keep-alive (h1) / multiplexed (h2) connections cached per host.
max_connections_per_host: 4,
// Timeouts are per-connect / per-read.
connect_timeout: Some(Duration::from_secs(10)),
read_timeout: Some(Duration::from_secs(60)),
// Automatic redirects (301/302/303 switch to GET, per RFC 9110).
max_redirects: 10,
// User-Agent sent on requests (None omits it).
user_agent: Some("my-app/1.0".to_string()),
// Fields added to every request this client initiates. A field on the
// request itself always wins; a cross-origin redirect drops
// authorization / proxy-authorization / cookie from either source.
default_headers: HeaderMap::new(),
// Defensive limits: header list and body size accepted from a peer.
max_header_list: 1 << 20,
max_body: 16 * 1024 * 1024,
};
let client = Client::with_config(cfg);
// Client is cheap to clone and shares the pools internally.
let c2 = client.clone();Client::new() is the same with all defaults.
let resp = client.get("http://127.0.0.1:8080/health")?;
println!("status: {}", resp.status.as_u16());
// Body::collect() blocks until the whole body arrives.
let body = resp.body.collect()?;
println!("body: {}", body.to_str()?);post takes anything that converts into a Body — Bytes, Vec<u8>, String, &'static str, or &'static [u8]:
let resp = client.post("http://127.0.0.1:8080/submit", "raw text payload")?;
let resp = client.post("http://127.0.0.1:8080/submit", vec![1u8, 2, 3])?;Client::request(url, method) returns a RequestBuilder: it builds the same Request the hand-written form below sends, and hands it to the same path, so redirects, pools and all three protocols behave identically. Client::{put, delete, head, patch, options} are the one-call shorthands.
use courierust::courierust_http::Method;
let resp = client
.request("http://127.0.0.1:8080/api/items", Method::POST)
.query([("page", "2")]) // appended to the URL, percent-encoded
.form([("name", "widget")]) // body + content-type, urlencoded
.header("accept", "application/json")
.basic_auth("user", "secret") // or .bearer_auth("token")
.timeout(std::time::Duration::from_secs(5))
.send()?;
println!("{}", resp.text()?);-
query/formuse the WHATWGapplication/x-www-form-urlencodedencoding (courierust_http::form): a space becomes+, anything outsideA-Za-z0-9*-._becomes%XX. -
timeoutoverridesClientConfig::read_timeoutfor this request: a transport deadline with the same meaning, applied per attempt and restored afterwards, so the connection returns to the pool with the configured value. It applies to h1, h2 and h3 alike. -
priorityis an RFC 9218 hint: HTTP/2 reads it (it feeds the WUCS scheduler), HTTP/1.1 and HTTP/3 have no field to carry it and send the request unchanged. -
resp.text()/resp.bytes()consume the body;textrefuses a body that is not valid UTF-8 rather than substituting U+FFFD.
use courierust::courierust_body::Body;
use courierust::courierust_bytes::Bytes;
use courierust::courierust_http::header::{HeaderName, HeaderValue};
use courierust::courierust_http::method::Method;
use courierust::courierust_http::request::Request;
let mut req = Request::new(Method::POST, "/api/items?page=2");
req.headers.insert(
HeaderName::from_lowercase("content-type"),
HeaderValue::from_static("application/json"),
);
req.headers.insert(
HeaderName::from_lowercase("authorization"),
HeaderValue::from_bytes(b"Bearer abc123")?,
);
req.body = Body::Bytes(Bytes::from(r#"{"name":"courierust"}"#));
let resp = client.execute("http://127.0.0.1:8080", req)?;
println!("version: {}", resp.version);
println!("x-request-id: {:?}", resp.headers.get("x-request-id"));Notes:
- The request's
uriis the path; the scheme/host/port come from the URL you pass toexecute. -
HeaderName::from_lowercaseis for lowercase static names;from_bytesvalidates and lowercases for you. - Response headers are order-preserving;
getreturns the first matching field.
Redirects are on by default and capped by max_redirects. 301, 302, and 303 switch the method to GET (RFC 9110) and drop the request body; 307 and 308 keep both the method and the body, and a body that only existed as a stream (so it cannot be replayed) fails the hop with an explicit error rather than being sent empty. Absolute, protocol-relative (//host/...), and relative Location values are all handled:
// Follows up to 10 hops automatically; the final response comes back.
let resp = client.get("http://short.example/start")?;For HTTP/2 you can attach a priority to each request. The server schedules streams with a WUCS scheduler: urgency 0..=7 (0 = highest), incremental for streams that can be consumed as data arrives.
use courierust::courierust_h2::priority::Priority;
// Parse from the wire format ("u=1, i") or build directly:
let prio = Priority { urgency: 1, incremental: true };
let mut req = Request::new(Method::GET, "/big-download");
let resp = client.execute_priority("http://127.0.0.1:8080", req, prio)?;Priority also implements Default (urgency 3, non-incremental) and Display (u=3), and can be parsed with Priority::parse(b"u=1, i").
A streaming (Channel) response body is consumed chunk by chunk with try_next_chunk — useful for SSE or long downloads without buffering everything:
let resp = client.get("http://127.0.0.1:8080/events")?;
let mut body = resp.body;
while let Some(chunk) = body.try_next_chunk()? {
// chunk: courierust::courierust_bytes::Bytes
eprintln!("chunk: {} bytes", chunk.len());
}Every fallible call returns courierust::Result<T> where the error is courierust::Error:
match client.get("http://127.0.0.1:9/") {
Ok(resp) => println!("ok: {}", resp.status),
Err(e) => {
println!("kind: {:?}", e.kind); // Error.kind is a public field
println!("message: {}", e);
}
}Error converts into std::io::Error (for use with ? in io-returning code) and carries a public kind field for programmatic handling.
-
HTTPS uses the built-in TLS 1.2 + TLS 1.3 implementation when configured. The default client has
tls: Noneand rejectshttps://; provide aRootStorethroughClientConfig::tlsor useClient::with_tls_roots. There is no bundled CA set. ALPN must agree withClientConfig::http2(h2for HTTP/2,http/1.1otherwise). -
HTTP/2 concurrency is connection-policy dependent. Requests are multiplexed by one driver per connection. Set
max_connections_per_hostabove one when independent HTTP/2 drivers are needed for caller-level parallelism; benchmark the full latency tail for the selected value. -
Streaming request bodies are HTTP/2-only.
Client::executematerializes aBody::Channelrequest body into memory before sending; useexecute_h2_streamfor true client-streaming uploads. -
The HTTP/1.1 keep-alive pool probes before it reuses. A pooled connection is checked for liveness first, so a server that closed it while it sat idle costs nothing — nothing is written, which is why even a non-idempotent method is safe on a fresh connection. If a pooled connection dies mid-request, the request is retried exactly once on a fresh connection and only when the method is safe to replay (RFC 9110 §9.2.2: GET/HEAD/OPTIONS/TRACE/PUT/DELETE/PROPFIND) — a
POSTreturns the failure instead of risking a second execution.
The same client speaks WebSockets — ws:// and wss://, the second through
the crate's own TLS stack — with ClientConfig supplying the read deadline:
use courierust::courierust_client::ClientConfig;
use courierust::courierust_client::ws::WebSocket;
let mut ws = WebSocket::connect("wss://example.com/ws", &ClientConfig::default())?;
ws.send_text("hello")?;
println!("{:?}", ws.read_message()?); // Event::Text("hello")
ws.close(1000, "done")?;It shares the framing / UTF-8 / close-handshake engine with the server, so both ends enforce the same rules; the tutorial (options, negotiation, deployment, honest notes) is WebSockets.