Networking
Introduction
Stub. The framework ships a small blocking HTTP helper (HttpRequestConfig, HttpResponse) you can call from inside a Thread. For raw sockets, async I/O, WebSockets, or anything else, do networking the same way you do any other blocking I/O: from a Thread.
What's available
-
HttpRequestConfig. A small blocking HTTP client. Configure timeouts, headers, max response size, and TLS verification, then callhttp_get,download_bytes, oris_url_reachable. The convenience constructorshttp_get_defaultanddownload_bytes_defaultskip configuration. -
HttpResponse. The result. Carriesstatus_code,body(U8Vec),headers,content_type,content_length. Useis_success,is_redirect,is_client_error,is_server_error,body_as_stringto inspect it. -
IrohEndpoint. Peer-to-peer QUIC connections between apps, for media frames and messages. See Peer-to-peer connections.
The framework is intentionally runtime-agnostic. There's no built-in raw-socket type and no async runtime integration. HTTP is request / resume shaped (below), so ordinary fetches need no worker thread; anything heavier still belongs in one.
Making a request
Every HttpRequestConfig call is a request: it returns a RequestId
immediately and resumes the callback you pass in with the answer. On desktop
the transfer runs synchronously inside the call and the callback runs right
after the requesting callback returns; in the browser it is a fetch() and
the callback runs on a later task. Either way the callback never runs
re-entrantly inside the callback that issued the request, and the same code
works on both.
extern "C" fn on_fetch_clicked(data: RefAny, _info: CallbackInfo) -> Update {
let cfg = HttpRequestConfig::create()
.with_timeout(10)
.with_user_agent("my-app/1.0");
let _request = cfg.http_get("https://example.org/api".into(), data, on_response);
Update::DoNothing
}
extern "C" fn on_response(mut data: RefAny, _info: CallbackInfo, result: RefAny) -> Update {
let Some(answer) = HttpGetResult::downcast(result).into_option() else {
return Update::DoNothing;
};
match answer.result {
ResultHttpResponseHttpError::Ok(resp) => {
// mutate the model through `data` the usual way
let _ = (&mut data, resp);
Update::RefreshDom
}
ResultHttpResponseHttpError::Err(e) => {
eprintln!("request failed: {e:?}");
Update::DoNothing
}
}
}
The result structs are HttpGetResult (for http_get, http_post and
http_request), HttpBytesResult (download_bytes) and
HttpReachableResult (is_url_reachable); each has a static
downcast(result) accessor. There is no default-config shortcut: build a
config with HttpRequestConfig::create() and call the method on it.
A request may be issued from a worker thread as well; its resume still runs
on the main thread. CORS applies in the browser and cannot be escaped: a
target that does not send Access-Control-Allow-Origin fails with
HttpError::Other naming CORS.
Reusing connections
A request opens a connection, uses it once and closes it. Many requests to the
same server each pay for a new connection and TLS handshake. To keep
connections open between requests, create an HttpClient once (in your app
state, not per request) and attach a clone to each config:
// once, e.g. when building the app state
let client = HttpClient::create(
HttpClientConfig::create().with_max_idle_connections_per_host(8),
);
// per request
let cfg = HttpRequestConfig::create().with_client(client.clone());
All clones share one pool, which closes its connections when the last clone is
dropped. With a client, TLS verification follows the client's
HttpClientConfig; the request's own timeout, headers and size limit still
apply. In the browser the client changes nothing: the browser pools
connections itself.
A pooled connection skips the TCP and TLS handshake, but not the DNS lookup: the host is resolved before the pool is asked for a connection. When many requests go to the same few hosts, let the client remember the answer:
let client = HttpClient::create(HttpClientConfig::create().with_dns_cache_secs(300));
The default is 0, one lookup per request. A host that changes its address
is reached again once its cached answer expires.
The same idea applies to threads. ThreadPool::create(n) starts n workers,
and pool.create_thread(...) returns an ordinary Thread whose body runs on
one of them, so many short jobs don't each start an OS thread.
Modelling connection state
Use a plain enum on the application data side. A typical shape:
enum ConnectionStatus {
Idle,
Connecting { thread_id: ThreadId, started: Instant },
Done { response: HttpResponse },
Failed { reason: String },
}
Cancel by calling event.remove_thread(thread_id) from a click handler. The thread destructor sends TerminateThread and joins. If your worker checks recv.recv() between operations, cancellation is prompt.
Using an async runtime
The framework doesn't host a runtime, but nothing prevents you from running one inside a Thread:
extern "C" fn tokio_worker(
_initial: RefAny,
mut sender: ThreadSender,
mut _recv: ThreadReceiver,
) {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
rt.block_on(async {
// futures-based code here; pump results through `sender`
});
}
A current-thread runtime keeps everything on the worker. Use a multi-threaded runtime if you need a worker pool, but spawn it once and reuse. Runtimes are expensive to construct.
Peer-to-peer connections
IrohEndpoint (module iroh) connects two apps directly over QUIC, with hole punching and an optional relay, using iroh. An endpoint is identified by its public key. A ticket is the dialing string that carries the key and the endpoint's current addresses; hand it to the other side as a link or QR code.
let endpoint = IrohEndpoint::bind(
IrohConfig::create("my-app/1").with_relay_mode(IrohRelayMode::Disabled),
);
extern "C" fn pump(mut data: RefAny, _info: TimerCallbackInfo) -> TimerCallbackReturn {
let endpoint = endpoint_of(&mut data);
while let Some(event) = endpoint.recv().into_option() {
match event.kind {
IrohEventKind::Ready => show_invite(&mut data, event.text),
IrohEventKind::PeerConnected => remember_peer(&mut data, event.peer),
IrohEventKind::Frame => draw_frame(&mut data, event.track, event.data),
IrohEventKind::Message => handle_message(&mut data, event.data),
IrohEventKind::PeerDisconnected | IrohEventKind::Error => report(&mut data, event.text),
}
}
TimerCallbackReturn::continue_unchanged()
}
connect(ticket)dials. The outcome arrives as aPeerConnectedorErrorevent.send_frame(peer, track, data)andbroadcast_frame(track, data)send each frame on its own QUIC stream. A frame that has not left yet is replaced by the next frame of the same track, so a slow link lowers the frame rate instead of adding latency. JPEG frames fromRawImage::encode_jpegare the simplest video format.send_message(peer, data)is reliable and ordered, for chat and control data.peer_stats(peer)reports whether the path is direct or relayed, the RTT, the congestion window and frame counters.- Nothing arrives unless you poll
recv, so drive it from a timer. - For rooms,
IrohLoadBalancerpicks the peers that forward media for everyone (backbone_size,select_backbone), andIrohTileRole::rendition_heightpicks the resolution a video tile should request.
The engine needs the dll's iroh feature, which build-dll enables; PlatformCapability::iroh() reports whether it is compiled in. In the browser the handle exists but does not bind yet. examples/azul-meet opens two windows that exchange camera and screen frames this way.
What this page doesn't cover
- TLS configuration beyond
disable_tls_cert_verification. For custom TLS stacks, userustls,native-tls, or a third-party HTTP client inside the worker. - Mid-frame cancellation of in-flight DNS or TCP handshakes.
std::netdoesn't expose this. Usesocket2or a third-party client if you need it. - WebSockets, gRPC, HTTP/2. Any blocking client works inside a
Thread.