# Courierust - [中文文档](https://github.com/blueokanna/Courierust/blob/main/README_CN.md)
<p align="center">
<img src="https://raw.githubusercontent.com/blueokanna/Courierust/main/assets/Courierust.png" alt="High-performance, self-contained Rust networking stack" width="20%">
</p>
> A self-contained HTTP/1.1 + HTTP/2 + HTTP/3 + WebSocket + gRPC protocol stack with zero third-party dependencies.
> Hands-on tutorials (English & 中文) live on the [wiki](https://github.com/blueokanna/Courierust/wiki).
The protocol core (`courierust_http` / `courierust_h1` / `courierust_hpack` / `courierust_h2` / `courierust_ws` / `courierust_deflate` / `courierust_quic` / `courierust_h3` / `courierust_fingerprint` / `courierust_crypto` / `courierust_bytes` / `courierust_io` / `courierust_error`) compiles under `no_std + alloc` with **no dependencies at all**. The `std` feature (on by default) layers the threaded networking on top: a work-stealing thread pool, TCP adapters, the from-scratch TLS 1.2/1.3 stack, channel-backed streaming bodies, the client (HTTP/1.1 pool, HTTP/2 and HTTP/3 drivers, WebSocket), the server (event-driven scheduler + WebSocket upgrade), and gRPC.
None of this wraps an existing library. Frame codecs, HPACK/QPACK header compression, QUIC packet protection, the WebSocket framing/masking/DEFLATE path, the stream state machine, flow control, priority scheduling, and fingerprint construction are all implemented from scratch, with no dependency on another HTTP stack.
## Why
The mainstream Rust HTTP ecosystem (hyper / h2 / h3 and friends) is excellent, but the dependency trees run deep, and things like `no_std` support, core affinity, and "what does this client look like to a server" are left as your problem. This crate is built around three constraints:
- **The protocol layer never touches `std`.** `std` only provides threads, TCP, and clocks.
- **Multi-core is explicit — within the model.** Server connections are dispatched through a work-stealing pool, and an **event-driven scheduler** (default on every platform) parks idle/partial plain-HTTP connections on a readiness poller so a herd of keep-alive / SSE / slow-loris connections cannot consume workers. Client pools are shared by authority, and HTTP/2 requests are multiplexed by a dedicated driver per connection; set `max_connections_per_host` when independent connections are needed for load distribution — at that cap the pool selects by **weighted load** (active streams + in-flight request-body bytes + a capped EWMA service time) and prefers an idle connection outright. Worker occupancy is **per connection**: a single HTTP/2 connection with many streams (or a slow stream, or SSE) holds exactly one server worker, so a connection's streams never multiply worker usage and never block each other.
- **The wire details follow the RFCs and are verified against published test vectors**, not just enough to pass a smoke test.
## Features
### Protocol core (no_std + alloc, zero deps)
- **HTTP/1.1** (`courierust_http` message model + `courierust_h1` wire codec): request/response parsing and serialization, keep-alive, chunked transfer, `100-continue` handling.
- **HTTP/2 (RFC 9113)**:
- Full frame codec (DATA / HEADERS / PRIORITY / RST_STREAM / SETTINGS / PUSH_PROMISE / PING / GOAWAY / WINDOW_UPDATE / CONTINUATION);
- Per-stream and connection-level flow control, windows advanced per frame;
- Stream state machine following §5.1 strictly — illegal transitions end in `PROTOCOL_ERROR`;
- Stream priorities (RFC 9218): parses the `Priority` header and `PRIORITY_UPDATE` frames (type `0x10`), backed by the built-in **WUCS scheduler** (below).
- **HPACK (RFC 7541)**:
- 61-entry static table + dynamic table + hash-accelerated index lookups;
- 8-bit two-level table-driven Huffman decode (built at compile time), fast path for short codes;
- Byte-for-byte verified against the official RFC C.2–C.6 vectors.
- **Fingerprints**:
- `TlsProfile` describes the parameters of a TLS ClientHello; includes self-contained MD5 / SHA-256 (no deps);
- **JA3**: `ja3_hash()` produces the standard 32-hex-digit fingerprint, matching the published Chrome record;
- **JA4**: `ja4()` produces the four-part `t13d1516h2_…` fingerprint, matching the spec example;
- **Chrome HTTP/2 fingerprint**: SETTINGS entries, initial `WINDOW_UPDATE`, frame order, and header ordering all mirror Chrome behavior, ready to feed to an external TLS layer.
### std networking layer
- **Work-stealing thread pool** (`courierust_pool`): per-worker LIFO cache + global FIFO steal queue; jobs can spawn jobs; stealing prefers the worker idle the longest.
- **Client** (`courierust_client`):
- HTTP/1.1 keep-alive connection pool grouped by authority with bounded reuse;
- HTTP/2 connections multiplex streams through dedicated drivers and can be capped per authority; at `max_connections_per_host` the pool picks the least-loaded connection (active streams + in-flight request-body bytes + a capped EWMA service time) and prefers an idle one outright;
- Redirect following (301/302/303 → GET), timeouts, `User-Agent`, etc.
- **Server** (`courierust_server`): by default an **event-driven scheduler** accepts, classifies (TLS / h2 / h1 from the first bytes), and parks idle plain-HTTP connections on a readiness poller (Winsock `select` / POSIX `poll`), handing ready ones to event workers in batches. TLS and HTTP/2 connections run on the blocking work-stealing pool. Setting `event_driven: false` restores the legacy one-pool-job-per-connection model for comparison.
- **WebSocket** (`courierust_ws` plus the server and client integrations): RFC 6455 framing, masking, UTF-8 validation, the opening handshake, fragmentation reassembly, the close handshake, and RFC 7692 `permessage-deflate` — from scratch. The server upgrades a live HTTP/1.1 connection from the handler hook (`Handler::websocket` → `WsService` + `WsConfig`: Origin policy, subprotocols, frame/message/fragment caps, a bounded send queue, Ping/Pong keepalive), and the upgrade works in **both** drivers — in the event reactor an idle WebSocket costs a poller slot instead of a thread. The client is `courierust_client::ws::WebSocket` over `ws://` and `wss://` (through the crate's own TLS). Engine details, deployment recipes and the honest benchmark rows: [`src/courierust_ws/README.md`](https://github.com/blueokanna/Courierust/blob/main/src/courierust_ws/README.md) and [`benches/WS_BENCHMARK.md`](https://github.com/blueokanna/Courierust/blob/main/benches/WS_BENCHMARK.md).
- **gRPC** (`courierust_grpc`): HTTP/2 + length-prefixed message framing + `grpc-status` / `grpc-message` handling, with unary, server-streaming, client-streaming and bidi calls on both sides. `gzip` message compression is implemented from scratch (RFC 1951/1952: full DEFLATE decompression for any producer, fixed-Huffman LZ77 compression) and negotiated per gRPC A6. Deadlines (`grpc-timeout`) are enforced server-side, metadata and interceptors are supported, `dns:///` targets round-robin, and the `grpc.health.v1.Health` service provides `Check` and `Watch`. Protobuf is deliberately left to you — implement `EncodeMessage` / `DecodeMessage` for your types, or use the raw-bytes API.
- **Streaming bodies** (`courierust_body`): channel-backed `Body::Channel` lets handlers push response chunks from another thread.
## The parts that actually took work: multi-core and scheduling
A `no_std` protocol core is a weekend project. Making it pay off across cores is not.
### WUCS — Weighted-Urgency Calendar Scheduler (RFC 9218)
RFC 9218 replaces the old dependency-tree model with 8 urgency levels. We implement it as a calendar scheduler over 8 buckets:
- Each bucket is a **DRR (Deficit Round Robin)** class with a byte quantum, so a busy high-urgency bucket cannot starve lower-urgency traffic (RFC 9218 §10 explicitly requires anti-starvation);
- **Incremental** streams inside a bucket are served round-robin (bandwidth is shared as data arrives); **non-incremental** streams are FIFO by stream ID, matching the RFC's "ascending stream ID" recommendation;
- The per-frame choice is **O(1)**: a fixed 8-bucket scan, no sorting, no heap — cheap enough to run every frame on a hot connection.
A `Priority { urgency, incremental }` can be parsed from the `Priority` header / `PRIORITY_UPDATE` frame, or passed directly via `Client::execute_priority`.
### BCR — Batched Credit Reflow flow control
The naive implementation replies with a `WINDOW_UPDATE` per frame, and control-frame overhead adds up. BCR accumulates received data and returns credit in batches, cutting control frames by roughly an order of magnitude.
### Connection ownership and scheduling
Each client connection owns its codec buffers and, for HTTP/2, one driver thread that serializes wire access while multiplexing streams. Pool bookkeeping is bounded by authority and `max_connections_per_host`; it is not a promise that one HTTP/2 connection scales linearly with caller threads. Use the concurrency benchmark and the full latency tail before choosing a connection count for a deployment.
### The event scheduler is a self-pipe, not a sleep-and-scan loop
The default server path is an accept thread + an event-loop thread + a set of event workers. The trap: the event loop blocks in `select`/`poll`, but _control messages_ (new connection, re-register a connection a worker just served) travel on an mpsc channel. If the loop only notices them on the next poll tick, every keep-alive round trip pays a full poll timeout (that was the original ~5 ms P99 spike). The fix is a **self-pipe**: a loopback socket pair whose read end is registered in the poller, so the accept thread and any worker can interrupt a blocking poll with one byte the instant a message is queued. Socket readiness (a client sending data) already wakes the poll immediately; with the self-pipe, _message_ wakeups are immediate too, and the poll timeout only bounds the wait when nothing at all is happening — it is not in the request-latency path. Ready connections are dispatched to workers in **batches** (one channel message per 16 ids), and on Windows the `select` batching gives every batch after the first a zero timeout so a ready socket in batch _k_ is never delayed by the timeouts of batches 0..k-1.
Slow-loris and idle-herd protection is enforced before workers are ever involved: an incomplete request parks on the poller (zero workers), connections idle for `idle_timeout` are reaped, and `max_connections` caps the parked population outright.
The reactor's wait set is kept honest by construction: a closing connection hands its socket handle to the event loop, which **unregisters the descriptor before closing it**, so a wait never names a closed socket (Winsock's `select` fails an entire wait set for one bad descriptor, while POSIX `poll` reports just that entry). Should a wait ever fail anyway, the loop rebuilds the wait set from its connection registries and backs off instead of spinning — recoveries are counted in `Stats::event_wait_errors`, which is zero in a healthy run (`courierust_server` README, `src/courierust_server/README.md`).
Per-request **stage timing** is built into the event path: `COURIERUST_H1_TRACE=1` emits `H1SEG|...` rows decomposing a request into accept→registered, registered→first-pickup, the keep-alive reactor round trip, worker→first-read, parse, handler, build, and write. On loopback the dominant terms are the reactor round trip and the socket write — the parser and handler are single-digit microseconds, which is the empirical answer to "is the time in the parser or in the handoff". See `courierust_server` README.
## Security hardening
This crate treats parsers as attack surface. Beyond the usual limits (header/line/body caps everywhere), the notable defenses:
- **Request smuggling (CWE-444).** Duplicate `Content-Length` with differing values is rejected; `Transfer-Encoding` is parsed as a codeword list where `chunked` must be the final, single occurrence (`Transfer-Encoding: notchunked`, `chunked, gzip` and empty codewords are all rejected); a request line must be exactly three tokens. Critically, the **blocking and the event-driven incremental parsers share the same chunk-size parser and framing rules** — two code paths that disagree on a request's meaning are exactly how smuggling happens behind a proxy, so there is exactly one authority.
- **TLS record layer.** Ciphertext length bounds, padding validation, inner content-type checks, and per-direction sequence numbers (tampered records fail `bad_record_mac`). TLS 1.2 shares the AEAD discipline: only AEAD suites are implemented (RFC 5246 §6.2.3.3 AAD framing; CBC/HMAC, RC4 and static-RSA are never offered), and Finished `verify_data` is compared in constant time on both versions. The decrypted handshake buffer is capped at the protocol's 16 MiB maximum so a peer streaming endless handshake records cannot grow memory without bound. Handshakes run under a dedicated `handshake_timeout` (10 s default) on both client and server, so a peer that connects and stalls mid-handshake releases its worker/caller instead of holding it for the full read timeout.
- **TLS trust.** Chain validation (validity, name chaining, signatures, CA/key-usage, trust anchor), RFC 6125 hostname matching including IP SANs and single-wildcard, and EKU enforcement (a leaf with an EKU extension must permit `serverAuth`). `verify: false` exists for testing and truly-unanchored peers and still verifies `CertificateVerify` + `Finished` — the handshake stays cryptographically sound.
- **HTTP/2.** HPACK bombs (integer overflow, header-list cap, dynamic-table size, Huffman EOS/padding) are rejected; flow-control windows are checked per frame at stream and connection level (overflow is `FLOW_CONTROL_ERROR`); DATA on bodyless messages, `content-length` mismatches at stream end, and RST on idle streams are all stream/connection errors; `SETTINGS_TIMEOUT` and keepalive dead-peer detection close silent peers.
- **Redirects never forward `Authorization` / `Cookie` across origins** (RFC 9110 §15.4).
- **WebSocket.** Mask direction is enforced in both directions (a server fails an unmasked client frame, a client fails a masked server frame — §5.1); `OriginPolicy::SameOrigin` is the default, so a browser page on another site cannot open an authenticated socket with the session cookies; `X-Forwarded-For` / `X-Forwarded-Proto` are believed only from a `trusted_proxies` network, so a client cannot spoof its own address or claim TLS; the handshake accepts exactly one canonical-length `Sec-WebSocket-Key`, `Version: 13`, token-list `Connection: Upgrade`, gated RSV bits and minimal-length length encodings; every limit (`max_frame` / `max_message` / `max_fragments` / `max_send_queue`) fails with the RFC-mandated code (1009/1007/1002) instead of buffering, and a decompression bomb hits 1009 rather than OOM. Masking keys come from a ChaCha20 stream seeded by platform entropy — one per frame, not a counter.
## Quick start
### Client
```rust,no_run
use courierust::courierust_client::Client;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// GET
let resp = client.get("http://127.0.0.1:8080/")?;
println!(
"status={} body={}",
resp.status,
String::from_utf8_lossy(&resp.body.collect()?)
);
// POST
let resp = client.post("http://127.0.0.1:8080/submit", "hello".as_bytes())?;
println!("status={}", resp.status);
Ok(())
}
```
Opt into HTTP/2 (h2c prior knowledge) and set priorities:
```rust,no_run
use courierust::courierust_body::Body;
use courierust::courierust_client::{Client, ClientConfig};
use courierust::courierust_h2::priority::Priority;
use courierust::courierust_http::method::Method;
use courierust::courierust_http::request::Request;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut cfg = ClientConfig::default();
cfg.http2 = true; // h2c prior knowledge
let client = Client::with_config(cfg);
let prio = Priority { urgency: 1, incremental: true };
let req: Request<Body> = Request::new(Method::from("GET"), "/api");
let resp = client.execute_priority("http://127.0.0.1:8080/api", req, prio)?;
println!("status={}", resp.status);
Ok(())
}
```
### Server
```rust,no_run
use courierust::courierust_body::Body;
use courierust::courierust_http::request::Request;
use courierust::courierust_http::response::Response;
use courierust::courierust_server::{Server, ServerConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut cfg = ServerConfig::default();
cfg.http2 = true; // serves h2c and h1.1 on the same port
let server = Server::bind_with_config("127.0.0.1:8080", cfg)?;
server.serve(|req: Request<Body>| -> Response<Body> {
let mut resp = Response::with_status(200.into());
resp.body = Body::Bytes(format!("path: {}", req.uri.as_str()).into());
resp
})?;
Ok(())
}
```
### gRPC
```rust,no_run
use courierust::courierust_bytes::Bytes;
use courierust::courierust_grpc::{GrpcClient, GrpcServer};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Server side: implement `Service`, or just pass a closure.
let server = GrpcServer::bind(
"127.0.0.1:50051",
|method: &str, req: Bytes| -> courierust::Result<Bytes> {
Ok(Bytes::from(format!(
"echo({method}): {}",
String::from_utf8_lossy(&req)
)))
},
)?;
let _handle = server.serve_background()?;
// Client side
let client = GrpcClient::new("http://127.0.0.1:50051")?;
let reply = client.call("helloworld.Greeter/SayHello", Bytes::from("world"))?;
println!("{}", String::from_utf8_lossy(&reply));
Ok(())
}
```
### WebSocket
```rust,no_run
use courierust::courierust_client::ClientConfig;
use courierust::courierust_client::ws::WebSocket;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// ws:// or wss:// — the TLS leg is the crate's own stack.
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")?;
Ok(())
}
```
The server side upgrades a live HTTP/1.1 connection: `Handler::websocket`
returns `WsUpgradeReply::Accept(service)` for the routes you own and
`WsUpgradeReply::Pass` for everything else, so plain HTTP and WebSocket
share one port and one handler.
```rust
use courierust::courierust_body::Body;
use courierust::courierust_http::request::Request;
use courierust::courierust_http::response::Response;
use courierust::courierust_server::ws::{WsConn, WsData, WsService, WsUpgradeReply};
use courierust::courierust_server::Handler;
use std::sync::Arc;
struct Echo;
impl WsService for Echo {
fn on_message(&self, conn: &mut WsConn, msg: WsData) {
match msg {
WsData::Text(t) => { let _ = conn.send_text(&t); }
WsData::Binary(b) => { let _ = conn.send_binary(&b); }
}
}
}
struct App;
impl Handler for App {
fn handle(&self, req: Request<Body>) -> Response<Body> {
let mut resp = Response::with_status(200.into());
resp.body = Body::Bytes(format!("path: {}", req.uri.as_str()).into());
resp
}
fn websocket(&self, req: &Request<Body>) -> WsUpgradeReply {
if req.uri.path() == "/ws" { WsUpgradeReply::Accept(Arc::new(Echo)) }
else { WsUpgradeReply::Pass } // the HTTP handler answers 404
}
}
fn main() {
// `App` is what you hand to `Server::serve`.
let _app = App;
}
```
`cargo run --example ws_echo` runs a server and a client against it in one
process (upgrade, text/binary round trips, a server-initiated push on the
same connection, subprotocol negotiation, a clean close);
`cargo run --example ws_client` exercises the production client options
(subprotocols, Origin, compression preference, read deadline) against any
`ws://` / `wss://` endpoint.
## HTTPS (built-in TLS 1.2 + TLS 1.3)
Since 0.1, the crate ships a from-scratch, zero-dependency TLS stack —
**TLS 1.3 (RFC 8446) and TLS 1.2 (RFC 5246 / RFC 8422)** — so
`https://` is a first-class capability of the same client and server:
```rust,no_run
use courierust::courierust_client::{Client, ClientConfig, TlsSettings as ClientTls};
use courierust::courierust_server::{Server, ServerConfig, TlsSettings as ServerTls};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Server: serve HTTPS from your certificate chain + private key.
// `from_pem_file` parses the chain and the key (PKCS#8, PKCS#1 or SEC1)
// and proves they belong together — a mismatched pair fails here, at
// startup, not on every handshake. Use `Identity::from_pem(cert, key)`
// or `Identity::from_der(chain, key)` when the pair comes from memory.
let server_cfg = ServerConfig {
http2: true, // h2 + HTTP/1.1 over TLS (ALPN)
tls: Some(ServerTls::from_pem_file("cert.pem", "key.pem")
.map_err(|e| e.to_string())?),
..Default::default()
};
// Client: trust your roots and enable TLS.
let mut roots = courierust::courierust_tls::RootStore::new();
roots.add_pem_file("ca.pem").map_err(|e| e.to_string())?; // or add_der / add_pem
let client = Client::with_config(ClientConfig {
tls: Some(ClientTls {
roots,
verify: true,
alpn: vec![b"h2".to_vec(), b"http/1.1".to_vec()],
now: 1_700_000_000, // certificate validity checks use this
..Default::default()
}),
..Default::default()
});
let resp = client.get("https://example.com/")?;
println!("status={}", resp.status);
// The same config is what a server binds with.
let _server = Server::bind_with_config("127.0.0.1:8443", server_cfg)?;
Ok(())
}
```
Supported TLS profiles:
- **TLS 1.3 (RFC 8446):** `TLS_CHACHA20_POLY1305_SHA256`,
`TLS_AES_128_GCM_SHA256`, `TLS_AES_256_GCM_SHA384`; X25519 key
exchange.
- **TLS 1.2 (RFC 5246 / RFC 8422):** AEAD-only ECDHE suites —
`ECDHE-ECDSA-AES128-GCM-SHA256`, `ECDHE-ECDSA-AES256-GCM-SHA384`,
`ECDHE-ECDSA-CHACHA20-POLY1305-SHA256` and the three `ECDHE-RSA-*`
twins (secp256r1 ECDHE). CBC/HMAC, static-RSA and RC4 suites are
never offered — the record layer only implements AEAD. The RFC 5746
`renegotiation_info` indicator is sent and echoed, and X25519 is
advertised only when TLS 1.3 is also offered (a TLS 1.2-only
ClientHello advertises secp256r1 only, so a TLS 1.2 server can never
select a group the client cannot complete).
Both versions share the same identity, certificate chain validation
and trust model: RSA-PSS / RSA-PKCS#1 v1.5 / ECDSA P-256 / P-384 /
Ed25519 certificate signatures; full X.509 chain validation (validity
windows, name chaining, signature verification, basic-constraints /
key-usage, RFC 6125 hostname matching incl. IP SANs and the
CVE-2025-61727 excluded-subtree wildcard rule, plus a pluggable root
store).
**The version window is fully configurable.** `TlsSettings::min_version`
/ `max_version` on both client and server (default `Tls12..=Tls13`)
control what is offered and negotiated. Pinning both to `Tls13` restores
a TLS 1.3-only policy; a TLS 1.2 server that accepted a TLS
1.3-capable client still writes the RFC 8446 §4.1.3 downgrade sentinel
into its ServerHello random so the client can detect the downgrade, and
a TLS 1.3-only client refuses a TLS 1.2 ServerHello with no silent
protocol downgrade. 0-RTT / early data are never offered; TLS 1.3
session resumption uses the standard 1-RTT PSK path (server-issued
session tickets with `psk_dhe_ke`).
For QUIC the ALPN must be `h3`; for HTTPS the ALPN must be `h2` or
`http/1.1`.
Run `cargo run --example https` for a self-signed end-to-end demo,
`cargo run --example h3` for an HTTP/3 (QUIC v1 + TLS 1.3) end-to-end demo
(cold connect vs pooled reuse, large-response flow control, concurrent
multiplexing, certificate rejection),
`cargo run --example grpc_streaming` for the gRPC streaming shapes
(server/client/bidi), deadlines, gzip compression negotiation and
metadata/interceptors, and `cargo run --example ws_echo` /
`cargo run --example ws_client` for the WebSocket server-and-client pair
and the client's production options.
## Fingerprints: making a connection "look like" Chrome
The TLS handshake parameters are fully yours to control (including via
the built-in TLS layer):
```rust
use courierust::courierust_fingerprint::{chrome_tls_profile, h2::ChromeH2Fingerprint, ja3_hash, ja4};
use courierust::courierust_hpack::HeaderField;
fn main() {
let profile = chrome_tls_profile();
assert_eq!(ja3_hash(&profile), "cd08e31494f9531f560d64c695473da9");
assert_eq!(ja4(&profile), "t13d1516h2_8daaf6152771_e5627efa2ab1");
// HTTP/2 side: the Chrome-shaped SETTINGS, the connection WINDOW_UPDATE
// and the header order.
let fp = ChromeH2Fingerprint::chrome();
let settings = fp.settings_entries(); // the 5 SETTINGS entries, in Chrome's order
assert_eq!(settings.len(), 5);
let _window_update = fp.connection_window_update; // Chrome's ~12 MiB connection WINDOW_UPDATE
let fields: Vec<HeaderField> = Vec::new();
let ordered = courierust::courierust_fingerprint::h2::order_headers_chrome(&fields);
assert!(ordered.is_empty());
}
```
## no_std usage
The protocol core does not require `std`:
```toml
[dependencies]
courierust = { version = "1.0.8", default-features = false }
```
Building with `--no-default-features` compiles only the protocol core, suitable for embedded / kernel contexts. The networking layer needs the `std` feature (the default).
## Limitations
Things this crate deliberately does not do:
- **HTTP/3 / QUIC has a dependency-free built-in path, with a declared protocol boundary.** `courierust_h3` runs HTTP/3 request/response over a std UDP reactor with QUIC v1 packet protection, the built-in TLS 1.3 adapter, ALPN `h3`, bounded CRYPTO/stream reassembly, Retry integrity and token-bound address validation, Version Negotiation, pre-validation 3x anti-amplification, ACK ranges, fresh-packet-number retransmission, RTT/RTO sampling, a bounded congestion window, control/QPACK streams, trailers, and GOAWAY validation. It is not yet a complete Internet QUIC implementation in the sense that 0-RTT and independent interop remain open, but the transport long tail is implemented and exercised: full PTO/time-threshold loss recovery, dynamic local `MAX_DATA`/`MAX_STREAM_DATA`/`MAX_STREAMS` credit updates, connection migration and path validation (PATH_CHALLENGE/RESPONSE), stateless reset (generation and validation), automatic bidirectional key update with the one-at-a-time guard, and QPACK blocked-stream acknowledgements (Section Acknowledgment / Stream Cancellation / Insert Count Increment on the decoder stream). Deliberately out of scope: 0-RTT / early data (replay protection is not taken on), and independent implementation interoperability — the quinn+h3 handshake interop gap is reported honestly in the benchmark suite rather than faked. Those two are the remaining items before advertising broad external interop.
- **TLS: no 0-RTT; mutual TLS is TLS 1.3 over TCP only.** TLS 1.3 session resumption is implemented at the TLS layer (server-issued session tickets, 1-RTT PSK via `psk_dhe_ke`, client-side session store keyed by hostname) and the pooled client caches one connector per authority, so a ticket captured on one connection is offered on the next (the benchmark rows still report `session_resumption=n/a`, which is about the benchmark, not the implementation). 0-RTT / early data are never offered. TLS 1.2 session ids are carried but never resumed. Client authentication **is** implemented, for TLS 1.3 over TCP: `ClientAuth::required`/`optional` on the server, `TlsSettings::identity` on the client, the chain validated against the client-auth roots together with its validity window and `clientAuth` EKU, possession proven by `CertificateVerify`, and `certificate_required` sent when a required client declines — the refusal is an alert on the wire, not a dropped socket. Combining it with TLS 1.2 (handshake setup) or with HTTP/3 (startup) is refused rather than half-served, and post-handshake authentication is out of scope. Both directions are covered by unit tests and by an integration test through the public `TlsSettings`/`client_auth` surface.
- **Event-driven server is default on every platform and HTTP/1.1-only.** `ServerConfig::event_driven` (default `true`) parks idle plain-HTTP connections on a readiness poller so a small worker pool serves many idle keep-alive / SSE / long-poll connections; TLS and HTTP/2 connections still use the blocking pool model (bounded by `handshake_timeout`, `h2_idle_timeout`, and worker count). Setting it to `false` restores the legacy **one-pool-job-per-connection** model; that path is deprecated for production use — it lets a herd of idle/slow connections exhaust the pool — and exists only for comparison and debugging. The default event path bounds resource use with `max_connections` (connection cap) and `idle_timeout`.
- **Streaming request bodies are only reliable over HTTP/2** (h2 frames naturally). Over HTTP/1.1, either send the whole body at once (`Body::Bytes`) or build chunked framing yourself.
- **gRPC does not include protobuf, `.proto` code generation, or `grpc.reflection`.** You implement the codec traits or wire in your own protobuf-generated code; reflection needs a protobuf schema inventory, which is external by design.
- **A synchronous handler that blocks for a long time holds a worker** (event-driven or not) — exactly as with any synchronous server; use channel response bodies for streaming. Worker occupancy is **per-connection, not per-stream**: on one HTTP/2 connection, any number of idle streams (SSE / long-poll / gRPC server-streaming) occupy the same single worker, and a slow stream never blocks its connection's other streams — both are covered by integration tests. A large herd of _connections_ is handled by the event scheduler (idle reaping + `max_connections`) rather than by adding workers.
- **WebSocket: RFC 6455 and RFC 7692 only.** RFC 8441 (WebSocket over HTTP/2) is not implemented: the `ws://`/`wss://` client offers **only** `http/1.1` in ALPN (even with `ClientConfig::http2 = true`) and refuses an h2 connection before reading a frame, while a WebSocket attempt on an established h2 connection is rejected server-side as a malformed message — a **stream error `PROTOCOL_ERROR`** (RFC 9113 §8.1.1), for both RFC 8441 extended CONNECT (`:protocol = websocket`, the rejection RFC 8441 §3 defines) and the HTTP/1.1-style `Upgrade`/`Connection` fields (§8.2.2) — so the connection and its other streams keep working. What never happens is a `200` on a request that could not become a WebSocket, or a connection that looks established and never carries a frame. In the event-driven driver a service callback runs on a reactor worker, so a bulk push loop from inside `on_message` blocks the reactor and eventually trips the bounded send queue; the supported fan-out path is `WsConn::sender()` from another thread. 256 KiB messages between two ends of this crate are slower than tungstenite's pairing (see [`benches/WS_BENCHMARK.md`](https://github.com/blueokanna/Courierust/blob/main/benches/WS_BENCHMARK.md), which reports the localised cause and the socket-deadline finding behind it) — small and medium messages are at parity or ahead.
- **HTTPS is first-class**: the client and server ship a from-scratch TLS 1.2 + TLS 1.3 implementation; `https://` needs a root store (supply your own — there is no bundled CA set). ALPN is enforced: a client configured for h2 speaking to a server that negotiates `http/1.1` — or that negotiates **no** ALPN at all — fails with a clear error instead of a silent protocol mismatch (RFC 9113 §3.3 requires ALPN `h2` over TLS).
- Redirects, keep-alive reuse, and friends prioritize correctness over aggressive tuning.
## Layout
Every public module is prefixed with the crate's name (`courierust_`) so no
module path collides with a third-party crate (e.g. `h2`, `http`, `bytes`,
`grpc`, `tls`):
```mermaid
flowchart TB
%% ==================================================
%% Protocol core: no_std + alloc, zero dependencies
%% ==================================================
subgraph CORE["no_std + alloc · protocol core (zero deps)"]
direction LR
HTTP["courierust_http<br/>HTTP message model"]
H1["courierust_h1<br/>HTTP/1.1 wire codec"]
HPACK["courierust_hpack<br/>HPACK"]
H2["courierust_h2<br/>HTTP/2"]
QUIC["courierust_quic<br/>QUIC v1"]
H3["courierust_h3<br/>HTTP/3"]
WS["courierust_ws<br/>WebSocket (RFC 6455 / 7692)"]
DEFLATE["courierust_deflate<br/>DEFLATE · gzip"]
FP["courierust_fingerprint<br/>JA3 · JA4 · Chrome H2"]
CRYPTO["courierust_crypto<br/>MD5 · SHA-1 · SHA-256 · Base64"]
BYTES["courierust_bytes<br/>Bytes · BytesMut"]
IO["courierust_io<br/>Read · Write traits"]
ERR["courierust_error<br/>unified error"]
end
%% ==================================================
%% Runtime: std only
%% ==================================================
subgraph RUNTIME["std · runtime & services"]
direction LR
TLS["courierust_tls<br/>TLS 1.2 / 1.3"]
NET["courierust_net<br/>TCP · poller"]
POOL["courierust_pool<br/>work-stealing pool"]
BODY["courierust_body<br/>channel-backed body"]
CLIENT["courierust_client<br/>h1 pool · h2/h3 drivers · ws client"]
SERVER["courierust_server<br/>event scheduler · ws upgrade"]
GRPC["courierust_grpc<br/>gRPC"]
end
%% ==================================================
%% Dependencies
%% ==================================================
HTTP --> H1
HPACK -.-> H2
QUIC --> H3
DEFLATE -.-> WS
CRYPTO -.-> FP
BYTES -.-> HTTP
BYTES -.-> H1
BYTES -.-> H2
BYTES -.-> H3
IO -.-> NET
H1 --> CLIENT
H1 --> SERVER
H2 --> CLIENT
H2 --> GRPC
H3 --> CLIENT
TLS --> CLIENT
TLS --> SERVER
NET --> CLIENT
NET --> SERVER
POOL --> SERVER
BODY --> CLIENT
BODY --> SERVER
classDef core font-weight:bold;
classDef runtime font-weight:bold;
class HTTP,H1,HPACK,H2,QUIC,H3,WS,DEFLATE,FP,CRYPTO,BYTES,IO,ERR core;
class TLS,NET,POOL,BODY,CLIENT,SERVER,GRPC runtime;
```
## Benchmarks
The `benches/` package is a self-contained suite (no `criterion` required) that reports throughput and the full latency tail — **P50 / P75 / P90 / P95 / P99** for every case:
- HTTP/1.1 keep-alive, sequential and multi-worker parallel;
- HTTP/2 multiplexing across many workers;
- HTTPS (TLS 1.2/1.3 + h2) end to end through the crate's own TLS stack;
- WebSocket (`--bench ws`): codec (encode/mask/decode/UTF-8), echo round trips and one-way push, against `tungstenite 0.30` and `tokio-tungstenite 0.30` in the same process — code, methodology and the honest rows are in [`benches/WS_BENCHMARK.md`](https://github.com/blueokanna/Courierust/blob/main/benches/WS_BENCHMARK.md);
- complexity (`--bench complexity`): **time and space scaling** — per-operation cost and allocation cost fitted across sizes (`cost = a + b·n`, plus the median of the adjacent-size-pair exponents that names the class and the worst pair beside it; every point is the minimum of three repeats) and the memory cost of one idle connection for the event-driven, blocking and hyper/tokio server shapes, each compared with `tungstenite` / `reqwest`+hyper where a fair counterpart exists — method, a sample run and the algorithmic class of every hot path are in [`benches/COMPLEXITY.md`](https://github.com/blueokanna/Courierust/blob/main/benches/COMPLEXITY.md);
- RFC 9218 priority scheduling;
- a concurrency model comparison (idle-connection herd vs. worker pool) and a slow-sender herd benchmark.
The benchmark workflow also records TLS end-to-end results (with a `TLSVERIFY` evidence row: `cert_verified`, `hostname_verified`, `negotiated_alpn`, `session_resumption`), reactor/connection/stream evidence (`STATS` rows: accepted/active connections, poll syscalls, wake-ups, event-queue depth, h2 streams, read/write syscalls), optional remote-host results from the `network` bench (including TLS and in-process rate-limiting scenarios), and `cargo-fuzz` parser runs. The workflow commits the regenerated [Github_Action_Benchmark.md](https://github.com/blueokanna/Courierust/blob/main/Github_Action_Benchmark.md) back to `main`, so the evidence lives in the repository rather than only in the Actions summary or an artifact.
```bash
cargo bench --manifest-path benches/Cargo.toml --bench throughput
cargo bench --manifest-path benches/Cargo.toml --bench concurrency
cargo bench --manifest-path benches/Cargo.toml --bench ws
cargo bench --manifest-path benches/Cargo.toml --bench complexity
cargo bench --manifest-path benches/Cargo.toml --bench interop
cargo bench --manifest-path benches/Cargo.toml --bench network
cargo fuzz run --fuzz-dir fuzz h2_frame -- -max_total_time=20
```
Every `RESULT|...` line carries `p50_us` … `p99_us`, and the report script (`scripts/generate_benchmark_report.sh`) turns them into a percentile table. These are loopback measurements; WAN / TLS / real-handler numbers depend on your deployment, which is exactly why the suite reports the full tail rather than a single mean.
The h2c client data is workload-specific, not a claim of universal leadership. The 1 KiB single-worker result is only a small comparison point; multi-worker results must be read with their connection policy and tail latency. The h2c large-body rows (1 MiB POST against the same hyper h2 server) are paced by the server's 64 KiB initial flow-control window (WINDOW_UPDATE round trips) and are **not valid for ratio claims** — reqwest retains a large fixed wait even with the async client, so the earlier "blocking-client artifact" framing was wrong.
**Pool semantics differ between the two clients and must not be conflated:** Courierust's `max_connections_per_host` caps *live* connections per authority; reqwest's `pool_max_idle_per_host` caps *idle pooled* connections. Setting both to the same N is only equivalent for a sequential workload — under concurrency reqwest may open more than N live connections.
**Worker-count guidance (measured, see the `STATS` rows):** HTTP/2 multiplexing sends all streams over one connection serviced by one driver thread. With `max_connections_per_host = 1`, throughput scales with workers up to ~4–8 and then _regresses_: 32 workers contend on the shared pool lock and the single driver's command channel faster than the driver can drain them. The `STATS` rows show `h2_connections=1` with `workers` concurrent streams — the serialization point. Prefer 4–8 client workers per h2 connection and scale connections, not workers, beyond that. When a per-authority connection cap forces a choice, the pool selects by **weighted load** — active streams plus in-flight request-body bytes (64 KiB units) plus a capped EWMA service-time term — so a connection carrying one 1 MiB upload is no longer mistaken for one carrying a header-only RPC (see `courierust_client` README).
**HTTP/3 latency tail (measured, `benches/src/h3.rs`):** the reactor is a poller-driven loop whose poll timeout is an *absolute protocol deadline*, not a fixed cadence, and whose ACK path is interactive — the first packet of every burst is acknowledged immediately and the rest coalesce into that ACK. This matters because a fixed poll tick used to gate every cwnd-limited round: each ACK was parked behind `ack_delay()` *and* the next poll wake, so a 64 KiB flow on loopback took ~5 ms per round. With immediate ACKs and deadline-folded polls, `h3_sequential` is p50 ~115 µs / max ~0.2 ms, `h3_parallel`×4 is p50 ~180 µs / p99 ~0.35 ms, a 64 KiB upload is p50 ~1.15 ms, and a 64 KiB download is p99 ~0.74 ms (single burst once cwnd has grown). `h3_ack_deferred` / `h3_credit_stalls` in `Stats` prove whether the batch window or the congestion window is pacing a flow. These are loopback numbers on one runner — compare on the same runner, as the `network` bench does across hosts.
## Interop evidence
The `benches` workspace also ships a dedicated **interop validation** suite
(`cargo bench --manifest-path benches/Cargo.toml --bench interop`) that runs
Courierust against the mainstream Rust HTTP stack over real sockets and
asserts correct semantics — not just performance:
- Courierust h1/h2c **client** → hyper h1/h2 **server**: path echo, POST
echo, keep-alive reuse, and h2 multiplexing (concurrent requests with
distinct paths must not be cross-wired);
- hyper-util h1/h2c **client** → Courierust **server**, and reqwest
(blocking, h1 and h2c prior knowledge) → Courierust **server**;
- 1 MiB request/response round-trips over h2c against a real hyper server
(flow-control window replenishment on both directions) and a slow-reader
sanity check.
- **HTTP/3 self-interop** (the H3 client and server are both this crate's;
there is no mainstream H3 peer in the workspace): GET/POST round trips,
pooled connection reuse, 256 KiB request/response flow control in both
directions, and concurrent stream multiplexing over one QUIC connection
— a loopback regression gate for the H3 path (`benchmarks` run in
`benchmark.yml` too).
This runs in CI on every PR (`benchmark.yml`), so a real interop regression
fails the pipeline. The mainstream crates are dev-only dependencies of the
bench workspace; the `courierust` library itself stays zero-dependency.
The `compare` bench also runs an **HTTP/3 comparison** against the
industry-standard **quinn + h3 crate**: both clients reuse one pooled QUIC
connection against the same Courierust H3 server and measure warm
per-request latency (1 KiB / 64 KiB). The quinn row is reported only when
the independent quinn/rustls QUIC/TLS handshake actually completes against
the Courierust server — where it does, both rows carry measured p50/p99;
where it does not (a genuine cross-implementation interop gap), the quinn
row is reported `not_available` with the failure reason, never faked. On
runners where the handshake completes, quinn's 1 KiB p99 is ~0.3 ms and
its 64 KiB p99 ~1.2 ms, which is the reference the Courierust rows are
measured against.
The self-interop suite only proves Courierust agrees with _itself_ on TLS.
To prove the TLS layer against an independent implementation, a separate
workflow (`tls-interop.yml`, script `scripts/tls_interop.sh`) drives
**OpenSSL `s_server`** (Courierust client → OpenSSL), **`curl` / `openssl
s_client`** (independent stack → Courierust server, h1 + h2 ALPN),
**nginx with HTTP/2** (Courierust h2 client → nginx) and the **rustls +
hyper peer** (`benches/src/tls_peer.rs`, h1 in both directions) against a
throwaway CA-signed certificate. The rustls rows carry the TLS version and
ALPN the peer itself printed — a row cannot claim a version the handshake
did not pick.
Loopback numbers can never tell you what the wire costs. A
`cross-machine.yml` workflow runs the identical `network` bench binary on
two **self-hosted runners on separate physical machines** (labels
`courierust-server` / `courierust-client`) and compares the resulting
`NETWORK|...` rows against a loopback baseline from the same binary — so
the rps/p99 gap between the two runs is the network path, not the
protocol stack.
## Tests
Counts below are per test binary, so they can be checked one-to-one against a run:
- **489 unit tests** (`cargo test --lib`): all HPACK RFC vectors (C.2/C.3/C.4/C.5/C.6), Huffman encode/decode (plus a decode output cap), frame codec, state machine, flow control, WUCS scheduling, JA3/JA4 comparison against published records, fingerprint parsing, TLS 1.3 handshake + RFC 8448 key schedule, TLS 1.2 handshake (ECDHE-RSA/ECDSA AEAD suites, PRF, RFC 5746 renegotiation echo, Ed25519 ServerKeyExchange signing/verification), X25519/Ed25519/ECDSA/RSA primitives, the DEFLATE/gzip codec (round-trips, CRC-32 vectors, corruption rejection, output-cap enforcement, cross-checked against Python zlib output, and the far-distance vectors that exercise distance codes 22-29), the **WebSocket engine** (mask phase tables, minimal-length encodings, control-frame rules, incremental UTF-8 validation, handshake parsing, the shared close flag, RFC 7692 negotiation), the poller's wake-descriptor (self-pipe) semantics and its closed-descriptor contract, the h2 pool's weighted-load accounting, the `application/x-www-form-urlencoded` codec (the WHATWG passthrough set, `+`/`%XX` round trips, refusal of malformed escapes and non-UTF-8), the field-value character class that h1/h2/h3 share, the `NewSessionTicket` wire format walked field by field against RFC 8446 §4.6.1 (the empty extension vector is still a vector), and the rule that an armed read deadline surfaces as `Timeout` on every platform, the PEM reader (armour rules, the three private-key containers), `Identity` loading (PEM/DER validation, a key that does not match its certificate, `Debug` printing the key's length instead of its bytes), the body-framing guard that refuses a message carrying both `Transfer-Encoding` and `Content-Length` (RFC 9112 §6.1 / CWE-444), and redirect resolution against the RFC 3986 §5.4 reference vectors.
- **89 integration tests** (`tests/integration.rs`): real loopback TCP round trips for h1/h2/HTTPS, keep-alive reuse, chunked, redirects, h2 concurrent multiplexing, streaming responses, large-body flow-control round trips, gRPC unary/server/client/bidi streaming + error status + trailers + deadline enforcement + gzip round-trip, `grpc.health.v1.Health` `Check` + `Watch`, RFC 7540 §3.2 `h2c` Upgrade, concurrency proofs (a slow stream does not block its connection's other streams; many idle streams consume one worker; an idle-connection herd does not block fresh requests; the event scheduler reaps slow-loris connections and enforces `max_connections`; server-streaming responses flush on a short cadence; one h2 connection serves a concurrent burst without command starvation), **request building** (every verb through `Client::request` and the shorthands, `query`/`form` encoding, basic/bearer auth, client default headers and the field that overrides them, default credentials stripped on a cross-origin redirect, per-request deadlines over h1 and h2 that leave the connection reusable, and a CR/LF-carrying header value refused by h1 and h2 alike), **h1 framing regressions** (a `Content-Length: 0` request is answered instead of parked; a HEAD response ends at its header block), and **TLS policy / hardening** (trust rejection, expired certificate, untrusted-issuer chain, self-signed-but-explicitly-trusted, hostname mismatch, ALPN agreement, TLS 1.2 + TLS 1.3 round trips with RSA / P-384 / Ed25519 identities, a TLS 1.3-only client refusing a TLS 1.2 server — no silent downgrade — and the RFC 8446 downgrade sentinel, interrupted-handshake failure, malformed-TLS-input survival, `verify:false`), and **PEM identity loading** (a server booted from the OpenSSL `tests/certs/*.pem` fixtures serves a request; a chain with an intermediate loads as two certificates; a key taken from another certificate is refused at load time), and the **request-smuggling guard** (a request carrying both framings is answered `400` by *both* drivers, with the bytes pipelined behind it never parsed as a second request) together with relative `Location` resolution against the request path (RFC 3986 §5.2).
- **14 HTTP/3 tests** (`tests/h3.rs` + `tests/h3_key_update.rs`): QUIC v1 + TLS 1.3 over real UDP sockets through the public `Client`/`Server` — GET/POST round trips, pooled connection reuse, 256 KiB request/response flow control in both directions, concurrent multiplexing, per-request deadline enforcement, a HEAD response that does not wait for the handler's streaming body, bidirectional key update, and H3 TLS security (untrusted / expired / wrong-chain / hostname-mismatch certificates all rejected at the handshake).
- **39 HTTP/2 hardening tests** (`tests/h2_hardening.rs`): hostile-frame inputs (oversized frames, malformed SETTINGS/PING/WINDOW_UPDATE, padding that overruns a field block, a stream-level zero-increment `WINDOW_UPDATE` staying a stream error, `WINDOW_UPDATE` on an idle stream, flow-control window overflow, HPACK header-list and Huffman bombs, truncated/EOS Huffman, pseudo-header ordering, `content-length` mismatches, forbidden `transfer-encoding`/`connection`-specific headers, a field value carrying NUL/CR/LF reported as a stream error rather than a connection error, `SETTINGS_MAX_CONCURRENT_STREAMS` enforcement on both ends, `h2c` liveness: SETTINGS_TIMEOUT and keepalive dead-peer detection).
- **37 WebSocket end-to-end tests** (`tests/ws.rs`): the real server, the real client and a real socket, covering the upgrade handshake (including the RFC 6455 accept-key vector), masking in both directions, fragmentation with interleaved control frames, `permessage-deflate` negotiation and RFC 7692 interop, UTF-8 failure codes, close-handshake cleanliness, `wss://` over the crate's TLS, push from another thread, client default headers on the handshake, Origin / subprotocol policy, the frame/message/queue limits, and the reactor regressions (a closed connection must not park the connections that are still open; a healthy reactor reports zero wait recoveries).
- **6 proxy tests** (`tests/proxy.rs`): the client against an HTTP proxy written with the standard library alone, so the client is the only implementation under test — a `CONNECT` tunnel for `https://` with the credentials visible to the proxy and absent at the origin, the absolute request form for `http://` (including `OPTIONS *` travelling as the empty-path absolute form, RFC 9110 §9.3.7), a request's own `Proxy-Authorization` winning over the configured one with exactly one field on that hop, a refused `CONNECT` surfacing the proxy's `403`, and `http3`/`h2c` + proxy refused before a socket is opened.
- **7 fuzz targets** (`cargo-fuzz`, `fuzz/fuzz_targets`): `h2_frame`, `hpack_block`, `h1_request` (the shared request/header/chunked path used by both server parsers), `h2_connection` (the full h2 state machine driven by hostile frame streams in both roles), `ws_frame`, `ws_handshake` and `ws_session`. The PR-time smoke run in `benchmark.yml` and the nightly long-fuzz workflow (`fuzz-long.yml`) exercise the first four with a wall-clock budget; the three WebSocket targets are type-checked by the satellite CI job and their assertions are mirrored by the deterministic property tests in `tests/ws.rs`.
- **5 documentation-parity tests** (`tests/readme_parity.rs`): `README.md` and `README_CN.md` must contain the same copy-pasteable code blocks (byte for byte), the same layout graph, and only absolute links (crates.io/docs.rs do not serve relative targets); every `src/<module>/README.md` / `README_CN.md` pair and every `wiki/en/*.md` / `wiki/zh/*.md` pair must contain the same code, comments aside.
- **README + wiki doctests**: every `rust` block in `README.md`, in each `src/<module>/README.md` and in each `wiki/en/*.md` page is compiled by `cargo test --doc`, so a sample that stops compiling fails CI instead of being copied by a user.
`benches/` and `fuzz/` are workspaces of their own, with their own lockfiles and target directories — the root `cargo test` / `cargo check --all-targets` never reaches them, while CI builds both (`cargo bench --manifest-path benches/Cargo.toml --locked --no-run`, `cargo check --manifest-path fuzz/Cargo.toml --all-targets`). They depend on the published crates they compare against, so they build on stable rather than the 1.78 MSRV. Adding a field to a public struct means compiling them too; the editor tasks `ci: benches all targets (locked)` and `ci: fuzz all targets` do exactly that.
```bash
cargo test # everything
cargo build --no-default-features # confirm the core compiles warning-free
```
## License
**PolyForm Perimeter License 1.0.1** — see [`LICENSE`](https://github.com/blueokanna/Courierust/blob/main/LICENSE), whose text is the official
[PolyForm Perimeter 1.0.1](https://polyformproject.org/licenses/perimeter/1.0.1) plus one additional
term the licensor adopted at the end.
What that means in practice:
- **Free to use, for any purpose except a competing product.** Reading, building, modifying,
self-hosting, embedding in an internal or customer system, teaching with it, shipping it in
non-competing software: all permitted. What is not permitted is providing to others a product
that substitutes for this one's functionality or value — including behind a service interface,
and including a port to another language ([Noncompete](https://polyformproject.org/licenses/perimeter/1.0.1/#noncompete),
[Competition](https://polyformproject.org/licenses/perimeter/1.0.1/#competition)).
- **Not an OSI-approved open-source license.** It is a *source-available* license: the source is
yours to read and change under the terms above, and it stays published under the same terms for
anyone who receives a copy from you (see *Notices*: keep this file with the distribution).
- **No warranty and no liability** for the software or its use, to the extent the law allows, and
an additional adopted term putting the same limit on unlawful use by anyone else (see
*Additional Term Adopted by the Licensor* at the end of `LICENSE`).