Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Nexora
Nexora is a Rust 2024 HTTP/1.1, HTTP/2, WebSocket, and TLS library for single-threaded Run-To-Completion applications. It combines Sans-IO protocols, compile-time declarations, and a nonblocking SDK built on liburing-rs and cares-rs.
Applications submit work and explicitly drive completion processing. Runtime handles remain !Send + !Sync
on their owning thread; callbacks can capture local state without Send bounds. No asynchronous executor is
required, and runtime ownership introduces no Mutex lock arbitration. Allocation, reference counting,
parsing, cryptography, and kernel operations retain their own costs.
Why Nexora?
A client worker may keep HTTP/1.1 connections warm, multiplex requests over HTTP/2, and consume a WebSocket feed while negotiating TLS for a replacement connection. Those paths share connection pools, deadlines, and business state. In a thread-per-core application, moving that state between independently scheduled tasks can complicate both latency control and ownership.
The usual Rust stack approaches these protocols through asynchronous APIs.
Hyper supports executor adapters, reqwest's asynchronous client uses Tokio,
and tokio-tungstenite integrates WebSockets with Tokio.
Multithreaded task APIs commonly add Send + 'static requirements; task handoffs
and work stealing can put scheduler decisions between received bytes and the
client's next action. Bringing that model into an existing synchronous worker
also means introducing asynchronous signatures or maintaining a bridge around
runtime-driven clients.
Nexora brings the protocols back under one worker's control. Its Sans-IO engines
parse HTTP, track HTTP/2 streams, assemble WebSocket messages, and advance rustls
sessions separately from transport I/O. The SDK joins those engines with DNS,
connection pools, and sockets on one caller-owned liburing::Proactor. Runtime
handles stay !Send + !Sync, and callbacks can retain worker-local state
without making it transferable to another thread. The application chooses when
to apply completions and advance each connection.
This separation is especially useful when a connection stalls. An HTTP/2 window
update, an interleaved WebSocket control frame, or an incomplete TLS handshake
can be inspected as an explicit protocol transition. Breakpoints follow ordinary
calls from completion dispatch into the relevant engine rather than repeatedly
crossing Future::poll and executor resume boundaries. The same structure gives
AI-assisted changes a concrete place to handle partial input, cancellation, and
buffer retirement, without inventing borrows or lifetimes across .await.
Nexora is designed for clients and shared-nothing workers that need that direct
control over a mixed-protocol connection lifecycle.
Capabilities
Core capabilities include:
- Zero asynchronous runtime dependencies across all three crates.
- HTTP/1.1 persistence, fixed-length/chunked bodies, bounded reassembly, and WebSocket handoff preserving coalesced post-header bytes.
- HTTP/2 multiplexing, HPACK/Huffman, signed flow-control windows, cancellation, and GOAWAY draining.
- RFC 6455 masking, fragmented messages, interleaved control frames, and closure.
- Ring-backed rustls TLS 1.3, verified identities, PEM/DER trust, mTLS, and ALPN.
- Shared-ring c-ares DNS, static overrides, origin pools, deadlines, and callback-originated submission through a local command inbox.
Architecture
Application: submit requests / implement services / retain session identifiers
|
nexora-macros: declarations and validated literals
|
nexora: HttpClient / HttpServer / WebSocketClient / callback contexts
| |
origin pools and DNS protocol/session state
| |
cares::ProactorResolver nexora-proto: HTTP1 / H2 / WS / TLS
| |
+------- liburing::Proactor ----+
one owning thread
|
Linux io_uring completions
| Crate | Responsibility | Dependency boundary |
|---|---|---|
nexora-proto |
Buffers, HTTP models, URI/headers, Sans-IO codecs and TLS | Independent of liburing, cares, macros, and SDK |
nexora-macros |
Typed construction, routing, WebSocket delegation, literal validation | Generates consumer-side references; no production SDK dependency |
nexora |
Proactor integration, DNS, pools, sockets, HTTP/WebSocket clients and servers | Depends on protocols and macros |
Each turn applies completions, advances explicit states, stages I/O and callback commands, then returns to the application. Scale across threads with a separate ring and separate runtime handles per thread.
Dependencies and platform
The checkout targets Linux x86_64 and pins its compiler in rust-toolchain.toml.
Runtime execution requires usable io_uring and network access. Native builds need upstream prerequisites; CI
installs CMake, Clang, libclang, pkg-config, and musl tools. Workspace packages have publish = false;
consume them from Git:
[]
= { = "https://github.com/trunkszi/nexora-rs.git", = "main" }
= { = "https://github.com/trunkszi/nexora-rs.git", = "main" }
Macros are re-exported by nexora. Generated protocol paths require a direct nexora-proto dependency even
when importing macros from the facade. Protocol-only consumers can depend solely on nexora-proto. Direct
macro users can add nexora-macros from the same source. #[json_body] additionally requires sonic-rs = { version = "0.5.10", default-features = false } as a direct application dependency.
The root manifest centralizes versions and tracks liburing, liburing-macros, cares, and cares-macros
through upstream Git main branches. Cargo.lock records selected revisions. TLS uses rustls 0.23 with
ring; sessions explicitly select TLS 1.3 even though the workspace enables rustls's TLS 1.2 code.
Buffers, HTTP models, and errors
Bytes provides immutable cheap-clone views. slice, split_to, and split_off share backing storage;
BytesMut::freeze transfers an immutable view without copying payload bytes. BytesMut supports cursor
advancement, resizing, growth, and big-endian put_u8/u16/u24/u32/u64. Mutation detaches shared backing
storage when necessary. Its default capacity is 4096 bytes.
use ;
Request owns Method, Uri, Version, Headers, and a complete Bytes body; Response owns
StatusCode, version, headers, and body. Constructors include Request::get/post, with_header,
with_body, and Response::ok/json/text. JSON responses accept already encoded bytes. Headers preserves
repeated fields, normalizes names to ASCII lowercase, and offers insert, append, get_all,
content_length, and is_chunked. Uri handles authority, port, path/query, and redirect resolution;
Scheme distinguishes HTTP, HTTPS, WS, and WSS.
Protocol boundaries use ProtoError, Http2ErrorCode, and nexora_proto::Result. Runtime boundaries use
nexora::Error and nexora::Result, preserving protocol, DNS, kernel, and I/O causes. Handle cancellation,
deadlines, BodyTooLarge, closed connections, exhausted pools, redirect limits, and invalid states
explicitly.
Sans-IO HTTP/1.1
Codecs transform caller-owned buffers without sockets. Decoders return Ok(None) for incomplete input and
retain bytes for the next call. Encoding validates a complete message before appending output.
use ;
Http1ClientConfig and Http1ServerConfig default to 16 KiB headers and 16 MiB bodies. Excessive bodies
return ProtoError::BodyTooLarge; excessive headers return HeaderListTooLarge. Framing accepts consistent
Content-Length or chunked coding, including extensions and validated trailers. Conflicting lengths and
ambiguous framing fail before consumption. HTTP/1.0 rejects Transfer-Encoding.
Http1KeepAlive reports KeepAlive, Close, or Upgrade; Http1Event represents completion/upgrade
events. Supply the request method when decoding responses for HEAD and CONNECT semantics. A 101 response or
WebSocket upgrade request consumes exactly through \r\n\r\n; pass the remaining buffer to WebSocketCodec.
Body-bearing upgrades are rejected. The decoder has no EOF signal and explicitly rejects EOF-delimited
response bodies; use fixed-length or chunked framing.
HTTP/2 multiplexing, compression, and flow control
Http2Connection owns the preface, SETTINGS exchange, compression contexts, streams, reassembly, and queued
output. Http2Config combines Http2Settings with a 16 MiB default body limit. Server push is disabled by the
connection layer.
use ;
Drain poll_event for messages, resets, body-limit events, SETTINGS/PING acknowledgements, and GOAWAY.
can_open_stream and active_stream_count expose admission; cancel_stream queues RST_STREAM(CANCEL).
send_ping, send_goaway, has_tx_bytes, and take_tx_bytes integrate with the transport. GOAWAY starts
draining already admitted streams while preventing inappropriate new admission.
Connection and stream send windows use signed i64. SETTINGS reductions can make stream windows negative;
queued DATA resumes after WINDOW_UPDATE credit. Inspect connection_send_window and stream_send_window to
diagnose stalls. Flow-control credit and body-size admission are separate limits.
The http2 module exports Http2Frame, Http2FrameHeader, Http2FrameType, protocol constants, encode_frame,
and decode_frame for lower-level integrations. HpackEncoder::encode appends header blocks;
HpackDecoder::decode validates them with table/header limits. Preserve each direction's compression
context. http2::huffman exposes huffman_encode, huffman_decode, and huffman_encoded_len; compression
helpers do not create transports or streams.
use ;
use ;
RFC 6455 WebSocket codecs
WebSocketHandshake builds and validates upgrades and computes accept keys. WebSocketCodec enforces endpoint masking
roles, text UTF-8, limits, and fragment ordering. WebSocketConfig defaults to 1 MiB frames and 16 MiB reassembled
messages.
use ;
WebSocketMessage represents Text, Binary, Ping, Pong, and Close. WebSocketCloseFrame carries WebSocketCloseCode and a
reason. Sans-IO callers own control responses and transport shutdown; runtime clients/servers add automatic
Pong and close progression. Keep the HTTP buffer remainder during upgrades, including a complete first
frame.
TLS 1.3 sessions and trust
TlsSession accepts fragmented/coalesced ciphertext and exposes plaintext and encrypted output. The caller
owns transport and handshake progress:
use ;
feed_ciphertext returns available plaintext, not consumed ciphertext bytes. write_plaintext may accept a
prefix: drain encrypted output and retry the suffix. Drain both handshake directions even without
application data. role, wants_read, and wants_write expose scheduling state. send_close_notify
closes local sending; drain accepted plaintext and alerts, allow peer half-close replies, then retire the
transport. Fatal TLS errors remain terminal; queued alerts can still be extracted.
| Configuration API | Policy |
|---|---|
TlsClientConfig::default() |
Public WebPKI roots; ALPN prefers h2, then http/1.1 |
with_root_certificates / with_pem_root_certificates |
Replace trust with DER roots or a PEM certificate bundle |
with_client_auth |
Add a DER client chain and private key |
TlsServerConfig::from_der |
Server chain/key without required client authentication |
from_der_with_client_auth |
Require a client identity trusted by supplied DER authorities |
self_signed_for_localhost |
Generate a local identity and return its DER trust certificate |
TlsClientConfig::insecure_client |
Test policy bypassing trust/name checks; permits impersonation |
Use verified roots and the expected server name in deployed clients. Select AlpnProtocol::Http2 for H2 and
Http11 for WebSocket upgrades.
Compile-time declarations
HeaderMap converts named fields through HeaderMapExt; values implement ToString/FromStr.
RestRequest generates into_request and build_path_and_query, percent-encoding path/query values.
Optional query, header, and body fields are omitted when absent.
use ;
use HeaderMapExt;
#[header(required)] requires an optional header; default handles absence. #[json_body] serializes
through the consumer's sonic_rs; #[body] accepts AsRef<[u8]>. Literal macros reject invalid names
and control-byte injection at compile time. http2_headers! preserves HTTP/2 pseudo-header spelling and field order.
#[http_service] and #[websocket_handler] generate the runtime implementations below.
Nonblocking HTTP client and callback re-entry
Use HttpClient::new with a Proactor and public ClientConfig, or HttpClient::builder().build(&mut ring) for defaults. Network and DNS subsystem IDs must be distinct, non-internal, and unique among owners
sharing the ring.
use ;
use ;
submit returns a RequestId; admitted requests have one terminal callback. Immediate admission errors
return directly. cancel reports whether a pending request was found. ClientContext stages submissions,
cancellation, timers, websocket_send, and websocket_close. Callbacks need neither a RefCell<HttpClient> nor a second
mutable borrow of the ring. Commands take effect on later progress.
ClientConfig controls pools, connect/request deadlines, redirects, header/body bounds, TLS, and H2
preference. Defaults include 10-second connect, 30-second request, five redirects, 16 KiB headers, and 16
MiB response bodies. Uploads default to unlimited; set max_request_body_size to bound them.
A zero max_body_size accepts only empty responses and does not restrict uploads.
Set tls_config: Some(TlsClientConfig::default()) for verified HTTPS. Cleartext H2 requires
http2_prior_knowledge; ordinary cleartext requests use HTTP/1.1.
Setting prefer_http2: false restricts HTTPS ALPN to HTTP/1.1, including when a
custom TLS policy otherwise offers HTTP/2. TlsClientConfig::with_alpn replaces
advertised protocols while retaining certificate trust and client identity.
A policy excluding http/1.1 rejects a peer that omits ALPN before sending any HTTP request.
HTTP/1.1 fallback requires that protocol to be allowed by the configured TLS policy.
Set tls_server_name: Some(name) to override TLS SNI and certificate hostname verification.
The connection target, request URI, HTTP/1.1 Host, and HTTP/2 :authority retain the original URI host.
Before dropping a client, call client.shutdown(&mut ring) with its live owning
proactor. It cancels pending requests, drains socket and timer completions, and
closes connection descriptors and retires DNS targets and cancellation acknowledgments. The call is
terminal and panics on proactor failure. Shared-ring owners use
client.shutdown_with(&mut ring, &mut peer_dispatcher)?; on failure retain both ring and driver and retry.
Origin pools and DNS
OriginKey::from_uri identifies normalized scheme/host/port origins. OriginPool is passive
admission/lifecycle state, not a socket owner. It coalesces waiters during resolution/handshake and manages
HTTP/1 slots and H2 stream admission. PoolConfig defaults to six connections/six idle entries per origin,
90-second idle expiry, and five-minute lifetime. Observe the client through active_request_count and
idle_connection_count.
HostResolver wraps cares::ProactorResolver on the same ring, static overrides, a TTL-bounded DnsCache,
and dual-stack queries. resolve_host pins addresses before DNS; an empty list removes a pin. IP literals
bypass name queries. dns_servers_csv selects servers; None reads system configuration. Shared queries
remain alive until their final waiter leaves; usable A/AAAA answers and remaining addresses drive connection
failover.
Direct integrations can use HostResolver::new/default_resolver, start_query, cancel_queries, flush,
on_completion, and cache accessors. Keep registered operands alive through final CQEs; shut down the ring
through the same dispatcher before dropping an armed resolver. begin_shutdown stops DNS submissions;
continue flushing and dispatching until has_pending_io() is false. Changing a cache or pin does not itself poll network work.
Unified HTTP services and deferred responses
Implement HttpService::call(&mut self, &mut RequestContext, Request, Responder) directly or generate
routing with #[http_service]. Context exposes route parameters, peer address, version, and H2 stream ID.
Immediate macro handlers return Result<Response>; deferred handlers own a Responder and return
Result<()>.
use ;
The deferred route retains a responder until a request on another connection calls /complete. Application work consumes the responder with
respond(response). Dropping an open unanswered responder queues 500; is_closed detects disconnects and
late respond returns ConnectionClosed. Gateways can retain it while submitting upstream work, then
answer on completion. Retaining it alone does not schedule application work.
ServerConfig controls limits, TLS identity, subsystem ID, backlog, and I/O buffer size. A cleartext
listener detects HTTP/1.1, H2C prior knowledge, and HTTP/1.1 WebSocket upgrades. A TLS listener selects H2
or HTTP/1.1 through ALPN, with WebSocket upgrades on HTTP/1.1. It does not sniff mixed plaintext/TLS.
HttpServer::drive applies caller-dispatched completions and submits I/O without polling the ring.
Standalone servers use explicit dispatch as above. HTTP/WS client drive methods own their DNS/network
routes; use client.drive_with(&mut ring, &mut server) when sharing with a server. Compose more owners
with CompletionRoutes::new(&mut first, &mut second), or implement CompletionDispatcher on a worker.
Unknown and overlapping routes fail closed. Drivers remain caller-owned; no handler is registered on the ring. initiate_graceful_shutdown starts draining;
continue progress before retiring the ring and its operands. IoUringSocket and SocketState expose
lower-level descriptor, buffer, in-flight, cancellation, and close lifecycle operations.
WebSocket clients, handlers, and proactive push
WebSocketClientConfig and WebSocketEvent live in nexora::websocket_client. Choose distinct subsystem IDs when combining
HTTP and WS clients; their defaults overlap. For WSS, supply verified TLS configuration offering ALPN
http/1.1.
use ;
use ;
The caller repeatedly invokes client.drive(ring). connect admits an attempt; its callback reports later
success/failure. Use is_open, send_text, send_binary, ping, pong, and close with the returned
WebSocketSessionId. Message callbacks receive a WebSocketSession; its send/ping/close methods stage work through
ClientContext. ping_interval enables configured keepalive pings.
Register server upgrades with server.with_websocket_handler("/ws", handler). Implement WebSocketUpgradeHandler or
generate event delegation:
use ;
;
Handlers select open, message, text, binary, ping, pong, close, and error events. WebSocketServerSession exposes
id, peer_addr, send, send_text, send_binary, and close. Save its ID during on_open; later
application turns can call server.websocket_send(id, message) or server.websocket_close for proactive push without
retaining mutable session borrows. The push example uses a caller-loop Instant deadline; callback timers
are available through ClientContext::set_timer.
Runnable examples
All four programs in crates/nexora/examples bind ephemeral loopback ports, assert results, and exit. They require real Linux io_uring; building an example does not establish successful runtime execution.
| Example | Demonstrated behavior |
|---|---|
http1_keepalive_and_cares_dns |
Two requests reuse one connection; an unpinned name queries a local UDP/TCP DNS responder |
http2_tls_multiplex_and_cancel |
Verified localhost TLS/H2, three completed requests, one pre-poll cancellation |
websocket_echo_and_push_timer |
Upgrade, text echo, deadline-triggered server push addressed by session ID |
unified_gateway_server |
Sequential HTTP/1.1, H2C, and WS clients share one cleartext listener |
The DNS example starts controlled mock-peer threads; library progress and callbacks stay on the application thread. The H2 example cancels before polling; it does not alone demonstrate an active on-wire reset. Lifecycle tests exercise the separate cancellation stages.
Build, validation, and test dimensions
Run commands from the repository root. .cargo/config.toml defaults RUST_TEST_THREADS
to 1, serializing ordinary harness execution and CI. Explicit environment values or --test-threads
override that default. This does not remove controlled peer threads within individual tests.
RUSTDOCFLAGS="-D warnings"
GitHub-hosted CI runs these full-workspace Cargo checks directly, including formatting, Clippy, tests, doctests, and rustdoc. Keep lockfiles in Cargo's native generated format. Dependencies use inline tables and authored files remain within 800 lines; generated Cargo.lock is exempt.
| Dimension | Command | Boundary and assertions |
|---|---|---|
| Integration | cargo test -p nexora --test integration_tests |
Buffers, protocols, macros, trust/ALPN, resolver integration, static pins, size rejection |
| E2E | cargo test -p nexora --test e2e_tests |
Real loopback SDK requests, multiplexing, callback chains, deferred responses, WebSocket exchanges |
| Lifecycle | cargo test -p nexora --test lifecycle_tests |
Exactly-once cancellation, completion retirement, responder fallback, drain, drop, resource release |
| Scenario | cargo test -p nexora --test scenario_tests |
Trickle/bursts, DNS retry/fallback, negative windows, resets, reconnect, slow peers |
Focused protocol and macro checks do not require a live application server:
Protocol targets label in-memory integration/E2E/lifecycle/scenario modules; macro targets include compile-pass and compile-fail fixtures. Runtime targets use controlled peers and real kernel I/O. Keep those evidence boundaries distinct: unavailable io_uring or native prerequisites are not passing runtime tests. Generate rustdoc for the complete public API, including lower-level inbox, DNS, pool, socket, and frame helpers. See AGENTS.md for maintenance rules.