Networking, HTTP & URL
Glide ships a complete networking stack as user-space stdlib: socket primitives, every common transport (TCP, UDP, multicast, raw, TLS, WebSocket), three generations of HTTP client (1.1, 2, 3), a routed HTTP/1.1 server, and the URL codec that glues them together. Everything is written in Glide on a single thin runtime syscall surface (gnet_* in netcore.c); there is no hidden C protocol code above the socket layer.
I/O follows the coro-blocking model. A call like accept, read, or connect blocks the calling coroutine, not the OS thread. On Linux the file descriptor is registered with the epoll reactor and the coroutine is parked until the kernel signals readiness, so the worker thread is free to run other coroutines in the meantime — one process can hold tens of thousands of live connections. On Windows and macOS the same surface falls back to blocking sync I/O: your code is identical and portable, but those platforms pin a worker per in-flight call until the async backends (IOCP / kqueue) land. You write straight-line code; the scheduler does the multiplexing.
Errors are values. Almost every fallible call returns !T (a Result), inspected through .ok plus .val on success or .err (a string) on failure — no exceptions, no panics on the happy-or-sad path. Check explicitly with if !r.ok { ... }, or bubble up with the ? operator inside a function that itself returns !T. Constructors that cannot fail (IpAddr::v4, HttpResponse::ok) return the value directly; everything touching the network or parsing untrusted input returns !T.
Import
// Core addressing + socket foundation
import stdlib::net::ip::*; // IpAddr, SocketAddr
import stdlib::net::sys::*; // Socket, Stream — the gnet_* foundation
import stdlib::net::sockopt::*; // SO_REUSEADDR / TCP_NODELAY / timeouts
import stdlib::net::dns::*; // resolve()
// Transports
import stdlib::net::tcp::*; // TcpStream (client)
import stdlib::net::listener::*; // Listener, Conn (server)
import stdlib::net::udp::*; // UdpSocket, UdpRecv
import stdlib::net::multicast::*; // IPv4 multicast join/leave
import stdlib::net::raw::*; // RawSocket (SOCK_RAW)
import stdlib::net::icmp::*; // ping, traceroute
import stdlib::net::tls::*; // TlsConfig, TlsStream (OpenSSL)
import stdlib::net::ws::*; // WebSocket, WsMessage
// HTTP client
import stdlib::http::client::*; // HttpClient, HttpResponse, http_get
import stdlib::http::cookies::*; // CookieJar
import stdlib::http::form::*; // FormData (x-www-form-urlencoded)
import stdlib::http::multipart::*; // Multipart (multipart/form-data)
import stdlib::http::jwt::*; // JwtClaims, sign/verify
import stdlib::http::h2::*; // HTTP/2 framing + GET client
import stdlib::http::hpack::*; // HPACK header compression
import stdlib::http::h3::*; // HTTP/3 over QUIC
// HTTP server
import stdlib::http::*; // HttpRequest, HttpResponse, http_listen
import stdlib::http::router::*; // Router, Route, Chain
import stdlib::http::route::*; // @route / @get / @post attrs, routes!()
import stdlib::http::handler::*; // @handler proc macro
import stdlib::http::extract::*; // FromRequest, Bearer, Path, Headers, Basic
import stdlib::http::typed::*; // Json<T>, JsonBind, into_response()
import stdlib::http::cors::*; // CorsConfig, CORS middleware
import stdlib::http::compress::*; // gzip_mw response compression
import stdlib::http::static::*; // static file serving
import stdlib::http::sse::*; // Server-Sent Events
import stdlib::http::proxy::*; // ReverseProxyConfig
// URL codec
import stdlib::url::*; // url_encode, url_decode
Module map
| module | what it gives you |
|---|---|
stdlib::net::ip |
IpAddr (IPv4/IPv6 tagged struct) and SocketAddr (ip+port) — build, parse, classify (is_loopback/is_private/...), and render to text (RFC 5952). |
stdlib::net::sys |
The first-class socket foundation: the Socket handle and the binary-safe Stream trait over the runtime's gnet_* syscalls. Every higher transport builds on this. |
stdlib::net::sockopt |
Uniform socket-option helpers (SO_REUSEADDR, TCP_NODELAY, recv/send timeouts) on any foundation .fd. |
stdlib::net::dns |
Hostname → IP resolution via the system resolver (getaddrinfo); resolve() returns !*Vector<*IpAddr>. |
stdlib::net::tcp |
TcpStream — outbound (client) TCP connect + read/write. |
stdlib::net::listener |
Server-side TCP: Listener (bind/accept) and Conn (accepted socket), wired to the async reactor. |
stdlib::net::udp |
UdpSocket + UdpRecv — connectionless datagram send_to / recv_from. |
stdlib::net::multicast |
IPv4 multicast: join/leave a group and tune multicast send behaviour on a UDP socket. |
stdlib::net::raw |
RawSocket (SOCK_RAW) for IP-layer payloads. Needs root / CAP_NET_RAW / Administrator. |
stdlib::net::icmp |
ping and traceroute, built in pure Glide on a raw socket. |
stdlib::net::tls |
TLS 1.2 / 1.3 client and server (TlsConfig, TlsStream) backed by OpenSSL; links -lssl -lcrypto. |
stdlib::net::ws |
WebSocket client (RFC 6455) over ws:// (TCP) or wss:// (TLS): WebSocket, WsMessage. |
stdlib::http::client |
The HTTP/1.1+2+3 client: HttpClient, HttpResponse, and one-shot http_get / http_post_* helpers. |
stdlib::http::cookies |
CookieJar for the client — parses Set-Cookie, replays Cookie:. |
stdlib::http::form |
FormData — application/x-www-form-urlencoded body builder + parser. |
stdlib::http::multipart |
Multipart — multipart/form-data builder for file uploads. |
stdlib::http::jwt |
JWT (JwtClaims) sign + verify over HMAC. |
stdlib::http::h2 |
HTTP/2 framing (RFC 7540) + the GET-only client transport (opt in with c.http2 = true). |
stdlib::http::hpack |
HPACK (RFC 7541) header compression used by the HTTP/2 transport. |
stdlib::http::h3 |
HTTP/3 client and server over QUIC (libngtcp2 / libnghttp3); opt in with c.http3 = true. |
stdlib::http |
The HTTP/1.1 server core: HttpRequest, HttpResponse, the ChunkSource streaming trait, and http_listen / https_listen / http_listen_workers. |
stdlib::http::router |
Method-aware router (Router, Route, Chain) with :param and *wildcard segments and middleware chains. |
stdlib::http::route |
@route(METHOD, "/path") + @get/@post/... sugar attrs and the routes!(r) auto-registration macro. |
stdlib::http::handler |
@handler proc macro — Axum-style typed handler params over the fn-pointer router shape. |
stdlib::http::extract |
FromRequest extractors: Bearer, Headers, Basic, Authorization<S>, Path<T>. |
stdlib::http::typed |
Typed JSON ergonomics: Json<T>, the JsonBind trait, and value.into_response(). |
stdlib::http::cors |
CorsConfig + CORS middleware for the router. |
stdlib::http::compress |
gzip_mw — gzip response-compression middleware (links -lz). |
stdlib::http::static |
Static file middleware: mount a directory at a URL prefix with ETag / 304 / index.html. |
stdlib::http::sse |
Server-Sent Events (text/event-stream) on top of ChunkSource. |
stdlib::http::proxy |
ReverseProxyConfig — forward requests to an upstream origin. |
stdlib::url |
URL percent-encoding: url_encode and url_decode (RFC 3986). |
IP & socket addresses — IpAddr
A single tagged struct holds either an IPv4 or an IPv6 address. There is no heap churn beyond the one malloc per address: v4 packs into a 32-bit field, v6 into two 64-bit halves. SocketAddr bundles an IpAddr with a port. Parsing is fallible and returns !T (a Result); read the outcome through .ok / .val / .err.
IpAddr
pub struct IpAddr {
pub kind: i32, // 4 or 6
pub v4: i32, // 32-bit host-order address, valid when kind == 4
pub v6_hi: i64, // upper 64 bits, host-order, valid when kind == 6
pub v6_lo: i64, // lower 64 bits, host-order, valid when kind == 6
}
kind is 4 or 6; the other fields are meaningful only for the matching kind. For v4, 127.0.0.1 packs to 0x7F000001. For v6, ::1 has v6_hi = 0, v6_lo = 1.
Constructors
IpAddr::v4(a: i32, b: i32, c: i32, d: i32) -> *IpAddr— from four octets.IpAddr::v6(hi: i64, lo: i64) -> *IpAddr— from two 64-bit halves (host-order).IpAddr::loopback_v4() -> *IpAddr—127.0.0.1.IpAddr::loopback_v6() -> *IpAddr—::1.IpAddr::unspec_v4() -> *IpAddr—0.0.0.0(bind-on-any-interface).IpAddr::unspec_v6() -> *IpAddr—::.
let ip: *IpAddr = IpAddr::v4(192, 168, 1, 100);
ip.to_string(); // => "192.168.1.100"
let one: i64 = 1;
IpAddr::v6(0, one).to_string(); // => "::1"
IpAddr::loopback_v4().to_string(); // => "127.0.0.1"
IpAddr::unspec_v4().is_unspecified(); // => true
Predicates
is_v4(self: *IpAddr) -> bool—trueif this is a v4 address.is_v6(self: *IpAddr) -> bool—trueif this is a v6 address.is_loopback(self: *IpAddr) -> bool—truefor127.0.0.0/8(v4) or::1(v6).is_unspecified(self: *IpAddr) -> bool—truefor the all-zeros address.is_private(self: *IpAddr) -> bool—truefor RFC 1918 (10/8,172.16/12,192.168/16) on v4 orfc00::/7unique-local on v6.
IpAddr::v4(8, 8, 8, 8).is_v4(); // => true
IpAddr::loopback_v6().is_v6(); // => true
IpAddr::v4(127, 0, 0, 1).is_loopback(); // => true
IpAddr::v4(8, 8, 8, 8).is_loopback(); // => false
IpAddr::v4(192, 168, 1, 5).is_private(); // => true
IpAddr::v4(8, 8, 8, 8).is_private(); // => false
Formatting & equality
to_string(self: *IpAddr) -> string— textual address; v6 follows RFC 5952 (longest zero-run collapsed to::).eq(self: *IpAddr, other: *IpAddr) -> bool— byte-for-byte equality. A v4 and a v6 value always compare unequal even when they refer to the same address.
IpAddr::v4(8, 8, 8, 8).to_string(); // => "8.8.8.8"
IpAddr::loopback_v6().to_string(); // => "::1"
let a: *IpAddr = IpAddr::v4(127, 0, 0, 1);
let b: *IpAddr = IpAddr::v4(127, 0, 0, 1);
a.eq(b); // => true
Parsing
IpAddr::parse(s: string) -> !*IpAddr— parse"1.2.3.4"(v4) or"::1"/"2001:db8::1"(v6). Returnserron malformed input. The presence of a:selects the v6 path.
let r: !*IpAddr = IpAddr::parse("192.168.0.1");
if !r.ok {
println!("parse failed:", r.err);
} else {
r.val.is_private(); // => true
}
IpAddr::parse("not.an.ip").ok; // => false
SocketAddr
pub struct SocketAddr {
pub ip: *IpAddr,
pub port: i32,
}
SocketAddr::new(ip: *IpAddr, port: i32) -> *SocketAddr— bundle an IP and port.to_string(self: *SocketAddr) -> string— render ashost:port; v6 hosts are wrapped in brackets per RFC 3986.SocketAddr::parse(s: string) -> !*SocketAddr— parse"1.2.3.4:80"or"[::1]:80".free(self: *SocketAddr)— release the wrappedIpAddrand theSocketAddrstruct itself.
let sa: *SocketAddr = SocketAddr::new(IpAddr::loopback_v4(), 8080);
sa.to_string(); // => "127.0.0.1:8080"
SocketAddr::new(IpAddr::loopback_v6(), 80).to_string(); // => "[::1]:80"
Round-trip a textual socket address, handling the error explicitly:
let r: !*SocketAddr = SocketAddr::parse("127.0.0.1:8080");
if !r.ok {
println!("bad address:", r.err);
return;
}
let sa: *SocketAddr = r.val;
defer sa.free();
println!(sa.port); // => 8080
SocketAddr::parse("[::1]:443").ok; // => true
DNS resolution — resolve
stdlib::net::dns turns a hostname into a list of IP addresses by wrapping the system resolver (getaddrinfo). There is no record type of its own — a lookup yields a *Vector<*IpAddr> reusing the IpAddr type from the IP section, with each entry carrying its own v4/v6 family.
import stdlib::net::dns::*;
The API is synchronous: a call blocks the calling coro until the OS responds. The runtime scheduler keeps other coros on the same worker running, so one slow query does not stall the whole app, but each lookup is still a blocking syscall. Resolution-bound workloads should fan out across coros via spawn / WaitGroup.
Error model
Every entry point returns !*Vector<*IpAddr> (a Result). Inspect .ok before reading .val; on failure .err holds the message:
let r: !*Vector<*IpAddr> = resolve("does-not-exist.invalid");
if !r.ok {
println!(r.err); // => dns: resolve failed for does-not-exist.invalid
return;
}
The only error produced by the resolver wrapper is dns: resolve failed for <host>, raised when getaddrinfo reports any failure. A successful call can still return an empty vector if the host has no A or AAAA records.
resolve
pub fn resolve(host: string) -> !*Vector<*IpAddr>
Resolves host to every A and AAAA record the system resolver returns, in the order the resolver returned them. Both v4 and v6 addresses are mixed into one vector; test each with is_v4() / is_v6().
import stdlib::net::dns::*;
let r: !*Vector<*IpAddr> = resolve("example.com");
if r.ok {
for let i: i32 = 0; i < r.val.len(); i++ {
println!(r.val.get(i).to_string()); // => 93.184.216.34, etc.
}
}
Using ? to propagate the error instead of branching:
fn first_addr(host: string) -> !string {
let ips: *Vector<*IpAddr> = resolve(host)?;
if ips.len() == 0 { return err("no addresses for ".concat(host)); }
return ok(ips.get(0).to_string());
}
resolve_v4
pub fn resolve_v4(host: string) -> !*Vector<*IpAddr>
Same as resolve but keeps only v4 (A) records. The result vector contains only addresses where is_v4() is true.
let r: !*Vector<*IpAddr> = resolve_v4("example.com");
if r.ok && r.val.len() > 0 {
println!("first v4:", r.val.get(0).to_string()); // => first v4: 93.184.216.34
}
resolve_v6
pub fn resolve_v6(host: string) -> !*Vector<*IpAddr>
Same as resolve but keeps only v6 (AAAA) records.
let r: !*Vector<*IpAddr> = resolve_v6("ipv6.google.com");
if r.ok && r.val.len() > 0 {
println!("first v6:", r.val.get(0).to_string()); // => first v6: 2607:f8b0:4005:80b::200e
}
Note that resolve_v4 and resolve_v6 both call resolve internally and filter the result, so a resolver failure surfaces identically through .err, and an empty vector means the host advertised no records of that family.
TCP — client & server
Raw byte-stream sockets. The client side is TcpStream (connect out to a remote host); the server side is a Listener that accepts Conns in a loop. Both go through the runtime's async reactor: on Linux a parked coroutine frees its worker while the kernel completes the handshake or fills the read buffer; on Windows/macOS/BSD the same surface runs as blocking sync I/O.
import stdlib::net::tcp::*;
import stdlib::net::listener::*;
import stdlib::net::ip::*;
For an HTTP server, reach for http_listen (see the HTTP server section) rather than hand-rolling a Listener accept loop — it does request parsing, keep-alive, and the writev header+body fast path for you. The primitives below are for plain TCP protocols.
TcpStream — outbound client
A connected stream socket. The only field is the file descriptor:
pub struct TcpStream {
pub fd: i64,
}
TcpStream also implements the Stream trait, so a value can be held as *dyn Stream alongside a TLS stream and shared by higher layers (ws, http).
TcpStream::connect(dst: *SocketAddr) -> !*TcpStream
Connect to an already-resolved address. Returns err if the socket can't be created or the handshake fails.
let dst: *SocketAddr = SocketAddr::parse("93.184.216.34:80").val;
let r: !*TcpStream = TcpStream::connect(dst);
if !r.ok { println!(r.err); return 1; }
let s: *TcpStream = r.val;
defer s.close();
s.write_all("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
let body: !string = s.read_to_end();
if body.ok { println!(body.val.len()); } // => byte count of the response
TcpStream::connect_host(host: string, port: i32) -> !*TcpStream
Resolve host through DNS and connect to the first reachable address on port, trying each returned address in order. Returns the last connect error if every candidate fails.
let r: !*TcpStream = TcpStream::connect_host("example.com", 80);
if !r.ok { println!(r.err); return 1; }
let s: *TcpStream = r.val;
defer s.close();
s.write_all("GET / HTTP/1.0\r\n\r\n");
stream.write(data: string) -> !i32
Write data once. May send fewer bytes than data.len() under back-pressure; the returned count is what actually went out. Use write_all when you need every byte delivered.
let w: !i32 = s.write("PING\r\n");
if w.ok { println!(w.val); } // => bytes written this call
stream.write_all(data: string) -> !i32
Write every byte of data, looping in the runtime until the payload is drained. Returns the total written.
let r: !i32 = s.write_all("HELLO server\r\n");
if !r.ok { println!(r.err); }
stream.read(max: i32) -> !string
Read up to max bytes. ok with a non-empty string is data; ok with a zero-length string means the peer closed the connection cleanly; err is a socket error.
let r: !string = s.read(4096);
if !r.ok { println!(r.err); return 1; }
if r.val.len() == 0 {
println!("peer closed");
} else {
println!(r.val);
}
stream.read_to_end(self) -> !string
Read repeatedly until EOF (the peer closes), accumulating everything. Useful for HTTP/1.0-style "read the whole response" exchanges.
s.write_all("GET / HTTP/1.0\r\n\r\n");
let r: !string = s.read_to_end();
if r.ok { println!(r.val); } // full response body
stream.set_timeout_ms(ms: i32)
Apply a combined read + write timeout in milliseconds via SO_RCVTIMEO / SO_SNDTIMEO. Pass 0 to disable. The client fd stays blocking so the timeout takes effect.
s.set_timeout_ms(5000); // 5s read/write deadline
let r: !string = s.read(4096); // err on timeout instead of hanging
Stream trait methods
Because TcpStream implements Stream, these binary-buffer methods are also available:
stream.read_bytes(buf: *u8, max: i32) -> !i32— read up tomaxbytes intostream.write_bytes(buf: *u8, n: i32) -> !i32— writenbytes frombuf.stream.close(self)— close the socket and free the struct. Afterclose()
the caller's buffer; returns the count (0 = peer closed).
the pointer is dangling.
let buf: *u8 = malloc(1024) as *u8;
let r: !i32 = s.read_bytes(buf, 1024);
if r.ok && r.val > 0 {
s.write_bytes(buf, r.val); // echo the bytes back
}
free(buf as *void);
Listener — server accept loop
Listener wraps a bound, listening socket; accept hands back a Conn per client. Both carry just an fd:
pub struct Listener { pub fd: i32 }
pub struct Conn { pub fd: i32 }
Listener::bind(port: i32) -> *Listener
Bind and listen on port across all interfaces. The returned Listener has fd < 0 on failure (port in use, missing privileges); always check ok() before accepting.
let l: *Listener = Listener::bind(8080);
if !l.ok() { println!("bind failed"); return 1; }
defer l.close();
println!("listening on :8080");
Listener::bind_reuseport(port: i32) -> *Listener
Same as bind, but sets SO_REUSEPORT where supported so multiple processes can share one accepting port and let the kernel load-balance accepts between them. On Windows this degrades to ordinary bind behaviour (SO_REUSEADDR only).
let l: *Listener = Listener::bind_reuseport(8080);
if !l.ok() { println!("bind failed"); return 1; }
defer l.close();
listener.ok(self) -> bool
true when the listener is bound and ready to accept.
listener.accept(self) -> *Conn
Block until a client connects, returning a Conn. The Conn has fd < 0 on error — check ok() first.
while true {
let c: *Conn = l.accept();
if !c.ok() { c.close(); continue; }
spawn handle(c); // one coroutine per connection
}
listener.close(self)
Close the listening socket and free the struct. Pair with defer.
Conn — an accepted connection
conn.ok(self) -> bool
true when the connection handshake completed.
conn.read(buf: *void, max: i32) -> i32
Read up to max bytes into buf. Returns the byte count, 0 when the peer closed cleanly, or -1 on error.
let buf: *u8 = malloc(1024) as *u8;
let n: i32 = c.read(buf as *void, 1024);
if n <= 0 { free(buf as *void); c.close(); return; }
conn.write(buf: *void, n: i32) -> i32
Write n bytes from buf. May write fewer than n — loop on the returned count for payloads larger than the kernel send buffer.
let mut sent: i32 = 0;
while sent < n {
let k: i32 = c.write(buf as *void, n - sent);
if k <= 0 { break; }
sent = sent + k;
}
conn.write_str(s: string) -> i32
Convenience: write a whole string (no trailing NUL is sent). Returns the byte count written.
c.write_str("HTTP/1.0 200 OK\r\n\r\nhi");
conn.write2(buf1: *void, n1: i32, buf2: *void, n2: i32) -> i32
Gather-write two buffers in a single writev / WSASend syscall — ship a header and a body without an extra copy or a second syscall. Returns total bytes written.
conn.sendfile(path: string) -> i32
Zero-copy file → socket transfer: sendfile() on Linux, TransmitFile() on Windows, a read+write loop on macOS/BSD. Disk pages go straight from the kernel page cache into the socket buffer. Returns total bytes sent (the file size on success) or -1 on error.
c.write_str("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n");
c.sendfile("public/index.html");
conn.close(self)
Close the connection and free the struct. The pointer is dangling afterward; do not touch it again. Pair with defer.
Full echo server
A complete line echo server: bind, accept in a loop, and spawn a coroutine per connection that reads and writes back until the peer closes.
import stdlib::net::listener::*;
fn handle(c: *Conn) {
defer c.close();
let buf: *u8 = malloc(4096) as *u8;
defer free(buf as *void);
while true {
let n: i32 = c.read(buf as *void, 4096);
if n <= 0 { return; } // EOF or error
c.write(buf as *void, n); // echo the bytes straight back
}
}
fn main() -> i32 {
let l: *Listener = Listener::bind(9000);
if !l.ok() { println!("bind failed"); return 1; }
defer l.close();
println!("echo server on :9000");
while true {
let c: *Conn = l.accept();
if !c.ok() { c.close(); continue; }
spawn handle(c);
}
return 0;
}
UDP & multicast — UdpSocket
Connectionless datagram sockets. Unlike TCP there is no stream and no handshake: every send_to carries its destination and every recv_from reports the sender. Datagram I/O is blocking in v1. The socket sits on the net::sys foundation, so a UdpSocket is just a wrapped file descriptor.
Almost every call returns a !T (Result); surface it via .ok / .err / .val, or short-circuit with ?.
UdpSocket
pub struct UdpSocket {
pub fd: i64,
}
The raw fd is exposed because the multicast helpers (and any low-level socket option you reach for) operate on the descriptor directly.
UdpSocket::bind(addr: *SocketAddr) -> !*UdpSocket
Bind a UDP socket to a local address. Pass IpAddr::unspec_v4() (or the v6 form) to listen on every interface. The address family is chosen from addr.ip automatically.
import stdlib::net::udp::*;
import stdlib::net::ip::*;
let any: *IpAddr = IpAddr::unspec_v4();
let addr: *SocketAddr = SocketAddr::new(any, 5353);
let r: !*UdpSocket = UdpSocket::bind(addr);
if !r.ok { println!(r.err); return 1; } // => bind failure text
let sock: *UdpSocket = r.val;
defer sock.close();
UdpSocket::bind_port(port: i32) -> !*UdpSocket
Convenience: bind on 0.0.0.0:port (all v4 interfaces).
let sock: *UdpSocket = UdpSocket::bind_port(5000).val;
defer sock.close();
UdpSocket::send_to(self, data: string, dst: *SocketAddr) -> !i32
Send data to dst. Returns the number of bytes sent. UDP doesn't fragment user-mode payloads, so keep each call ≤ 1472 bytes for safe v4 over Ethernet.
let dst: *SocketAddr = SocketAddr::new(IpAddr::v4(127, 0, 0, 1), 5000);
let s: !i32 = sock.send_to("ping", dst);
if !s.ok { println!(s.err); return 1; }
println!(s.val); // => 4
UdpSocket::recv_from(self, max: i32) -> !*UdpRecv
Block until a datagram arrives. max caps the payload size — bytes past max are silently dropped by the kernel. The result pairs the payload with the sender's SocketAddr.
let r: !*UdpRecv = sock.recv_from(1500);
if !r.ok { println!(r.err); return 1; }
let pkt: *UdpRecv = r.val;
println!(pkt.data); // => the bytes received
println!(pkt.from.port); // => sender's source port
UdpSocket::close(self)
Close the descriptor and free the struct. Pair with defer.
let sock: *UdpSocket = UdpSocket::bind_port(5000).val;
defer sock.close();
A full echo responder, binding any-interface, reading one datagram and replying to its sender:
import stdlib::net::udp::*;
import stdlib::net::ip::*;
let r: !*UdpSocket = UdpSocket::bind(SocketAddr::new(IpAddr::unspec_v4(), 9000));
if !r.ok { println!(r.err); return 1; }
let sock: *UdpSocket = r.val;
defer sock.close();
let got: !*UdpRecv = sock.recv_from(1500);
if !got.ok { println!(got.err); return 1; }
let pkt: *UdpRecv = got.val;
println!(pkt.data); // => "hello"
sock.send_to(pkt.data, pkt.from); // echo back to the source addr
UdpRecv
One received datagram: payload plus sender address.
pub struct UdpRecv {
pub data: string, // the datagram payload
pub from: *SocketAddr, // who sent it
}
from is reconstructed from the kernel's source address — IPv4 datagrams decode to a dotted-octet IpAddr, IPv6 to a 128-bit one — so you can reply with send_to(.., pkt.from) without tracking peers yourself.
Multicast
IPv4 multicast group membership and send tuning, layered on a bound UdpSocket. These are free functions that take the socket's fd rather than methods — bind the socket first, then join. The network-byte-order address packing is handled in the runtime, so you pass host-order IpAddrs.
mc_join_v4(fd: i64, group: *IpAddr, iface: *IpAddr) -> !i32
Join an IPv4 multicast group on the local interface iface. Use IpAddr::unspec_v4() for the default interface. The socket must already be bound.
import stdlib::net::udp::*;
import stdlib::net::multicast::*;
import stdlib::net::ip::*;
let sock: *UdpSocket = UdpSocket::bind_port(5353).val;
defer sock.close();
let group: *IpAddr = IpAddr::v4(239, 1, 2, 3);
let j: !i32 = mc_join_v4(sock.fd, group, IpAddr::unspec_v4());
if !j.ok { println!(j.err); return 1; }
let r: !*UdpRecv = sock.recv_from(1500); // now receives group traffic
if r.ok { println!(r.val.data); }
mc_leave_v4(fd: i64, group: *IpAddr, iface: *IpAddr) -> !i32
Leave a previously-joined IPv4 multicast group.
let l: !i32 = mc_leave_v4(sock.fd, group, IpAddr::unspec_v4());
if !l.ok { println!(l.err); }
mc_set_ttl(fd: i64, ttl: i32) -> !i32
Hop limit for outgoing multicast datagrams. Default 1 keeps traffic on the local subnet; raise it to route across routers.
mc_set_ttl(sock.fd, 4); // allow up to 4 router hops
mc_set_loop(fd: i64, on: bool) -> !i32
Whether multicast sent from this socket loops back to local members (default on). Turn it off when you don't want to receive your own sends.
mc_set_loop(sock.fd, false); // suppress local loopback of our own packets
A sender that joins a group, disables loopback so it won't see its own traffic, and emits one datagram with a wider TTL:
import stdlib::net::udp::*;
import stdlib::net::multicast::*;
import stdlib::net::ip::*;
let sock: *UdpSocket = UdpSocket::bind_port(0).val;
defer sock.close();
let group: *IpAddr = IpAddr::v4(239, 1, 2, 3);
mc_set_loop(sock.fd, false);
mc_set_ttl(sock.fd, 2);
let dst: *SocketAddr = SocketAddr::new(group, 5353);
let s: !i32 = sock.send_to("announce", dst);
if !s.ok { println!(s.err); return 1; }
println!(s.val); // => 8
TLS — TlsStream
TLS 1.2 / 1.3, client and server, backed by OpenSSL. The link line needs libssl + libcrypto — Glide adds -lssl -lcrypto unconditionally, so a missing OpenSSL surfaces as the usual cannot find -lssl link error.
- Windows:
mingw-w64-ucrt-x86_64-openssl(UCRT64 toolchain). - Linux / macOS:
libssl-dev/openssl@3.
SNI is set automatically from the host name passed to connect, and the system CA bundle drives verification. On Windows the CA chain is bridged from the ROOT + CA system stores via Crypt32, since the mingw OpenSSL build ships an empty OPENSSLDIR. Setting verify=false disables certificate validation — never do that in production.
import stdlib::net::tls::*;
let cfg: *TlsConfig = TlsConfig::client();
let r: !*TlsStream = TlsStream::connect("example.com", 443, cfg);
if !r.ok { println!(r.err); return 1; }
let s: *TlsStream = r.val;
s.write_all("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");
let body: string = s.read_to_end().val;
println!(body.len(), "bytes"); // => 1256 bytes
s.close();
TlsConfig
Configuration for an outbound TLS connection.
pub struct TlsConfig {
pub verify: bool, // validate peer cert + hostname (always true in prod)
pub min_proto: i32, // 0=TLS1.0 1=TLS1.1 2=TLS1.2 3=TLS1.3 minimum (default 2)
pub alpn: string, // comma-separated ALPN list, e.g. "h2,http/1.1"; "" disables
}
TlsConfig::client() -> *TlsConfig— default client config:verify=true, min TLS 1.2, no ALPN.TlsConfig::client_insecure() -> *TlsConfig— cert validation off; local dev only.cfg.free()— release the config struct.
alpn and min_proto are plain fields, so adjust them after construction:
let cfg: *TlsConfig = TlsConfig::client();
defer cfg.free();
cfg.alpn = "h2,http/1.1"; // advertise HTTP/2 then HTTP/1.1
cfg.min_proto = 3; // require TLS 1.3
Insecure config against a self-signed dev server:
let cfg: *TlsConfig = TlsConfig::client_insecure(); // DO NOT use in prod
defer cfg.free();
let r: !*TlsStream = TlsStream::connect("localhost", 8443, cfg);
if r.ok { defer r.val.close(); /* talk to the self-signed server */ }
TlsStream
A connected TLS stream. The single field is the opaque OpenSSL handle:
pub struct TlsStream {
pub handle: *void,
}
TlsStream also implements the Stream trait, so read_bytes / write_bytes / close are available alongside the inherent methods.
Constructors
TlsStream::connect(host: string, port: i32, cfg: *TlsConfig) -> !*TlsStream— resolvehostvia DNS, open TCP, run the handshake. The hostname feeds SNI and cert verification.TlsStream::attach(tcp: *TcpStream, hostname: string, cfg: *TlsConfig) -> !*TlsStream— run the TLS handshake on top of an already-connectedTcpStream(STARTTLS-style upgrade). On success the returned stream owns the fd; do not calltcp.close()afterwards — theTcpStreamwrapper is freed for you.
I/O
stream.write(data: string) -> !i32— writedata; returns bytes written (may be partial). NUL-truncating.stream.write_all(data: string) -> !i32— write every byte, looping on partial writes.stream.read(max: i32) -> !string— read up tomaxbytes;ok("")means the peer closed cleanly.stream.read_to_end() -> !string— read until the peer closes the connection.stream.read_bytes(buf: *u8, max: i32) -> !i32— binary-safe read intobuf(Stream trait).stream.write_bytes(buf: *u8, n: i32) -> !i32— binary-safe write (Stream trait); unlikewrite, does not truncate at NUL.
Connection info
stream.alpn() -> string— negotiated ALPN protocol, or""if none.stream.version() -> i32—2for TLS 1.2,3for TLS 1.3,0otherwise.stream.cipher() -> string— negotiated cipher suite name, e.g.TLS_AES_256_GCM_SHA384.stream.set_timeout_ms(ms: i32)— apply read + write timeouts on the underlying socket.
Teardown
stream.close()— shut down TLS, close the socket, free the stream (Stream trait).
A request that inspects the negotiated connection:
let cfg: *TlsConfig = TlsConfig::client();
cfg.alpn = "h2,http/1.1";
let r: !*TlsStream = TlsStream::connect("cloudflare.com", 443, cfg);
cfg.free();
if !r.ok { eprintln(r.err); return 1; }
let s: *TlsStream = r.val;
defer s.close();
println!("alpn:", s.alpn()); // => alpn: h2
println!("version:", s.version()); // => version: 3
println!("cipher:", s.cipher()); // => cipher: TLS_AES_256_GCM_SHA384
STARTTLS upgrade — connect in plaintext, negotiate, then promote the same connection:
import stdlib::net::tcp::*;
let tcp: *TcpStream = TcpStream::connect_host("smtp.example.com", 587).val;
tcp.write_all("EHLO me\r\nSTARTTLS\r\n");
// ... read the "220 Go ahead" reply on tcp ...
let cfg: *TlsConfig = TlsConfig::client();
let r: !*TlsStream = TlsStream::attach(tcp, "smtp.example.com", cfg);
cfg.free();
if !r.ok { eprintln(r.err); return 1; } // tcp is consumed; do not close it
let s: *TlsStream = r.val;
defer s.close();
s.write_all("EHLO me\r\n"); // now over TLS
Binary-safe framing over the Stream methods:
let s: *TlsStream = TlsStream::connect("example.com", 443, TlsConfig::client()).val;
defer s.close();
let frame: *u8 = malloc(9) as *u8; // 9-byte payload that may contain NULs
// ... fill frame ...
let w: !i32 = s.write_bytes(frame, 9);
if !w.ok { eprintln(w.err); }
let buf: *u8 = malloc(4096) as *u8;
let rd: !i32 = s.read_bytes(buf, 4096);
if rd.ok { println!("read", rd.val, "bytes"); }
TlsListener
Server-side listener: holds the SSL_CTX (cert + key) plus the listening TCP socket. accept blocks for the next client and returns a TlsStream once the handshake succeeds.
Constructors
TlsListener::bind(port: i32, cert_path: string, key_path: string) -> !*TlsListener— bind0.0.0.0:port, load the PEM cert (+ chain) and PEM private key, start listening.TlsListener::bind_alpn(port, cert_path, key_path, alpn_csv: string) -> !*TlsListener— same, but advertise ALPN protocols (CSV, server-preference order — first matching client offer wins).
Accepting
listener.accept() -> !*TlsStream— block for the next client, run the handshake, return a ready stream. Clients whose handshake fails are surfaced aserr.listener.accept_raw() -> !i64— TCP-only accept; returns the raw fd, leaving the handshake to the caller (typically on a worker thread). Keeps the accept loop from blocking on a slow or silent handshake.listener.attach(fd: i64) -> !*TlsStream— complete the handshake on a fd fromaccept_raw, reusing the listener's cert context. The fd is closed automatically on handshake failure.
Teardown
listener.close()— free the cert context and listening socket.
A minimal HTTPS-ish server loop:
let r: !*TlsListener = TlsListener::bind(8443, "fullchain.pem", "privkey.pem");
if !r.ok { eprintln(r.err); return 1; }
let l: *TlsListener = r.val;
defer l.close();
while true {
let a: !*TlsStream = l.accept();
if !a.ok { continue; } // skip failed handshakes
let s: *TlsStream = a.val;
s.write_all("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi");
s.close();
}
Advertise HTTP/2 via ALPN and branch on the negotiated protocol:
let r: !*TlsListener =
TlsListener::bind_alpn(443, "cert.pem", "key.pem", "h2,http/1.1");
if !r.ok { eprintln(r.err); return 1; }
let l: *TlsListener = r.val;
defer l.close();
while true {
let a: !*TlsStream = l.accept();
if !a.ok { continue; }
let s: *TlsStream = a.val;
if s.alpn().eq("h2") {
// speak HTTP/2
} else {
s.write_all("HTTP/1.1 200 OK\r\n\r\nhi");
}
s.close();
}
Offload the handshake to a worker so a stalled client can't park the accept loop:
let l: *TlsListener = TlsListener::bind(443, "cert.pem", "key.pem").val;
defer l.close();
loop {
let fd: !i64 = l.accept_raw();
if !fd.ok { continue; }
spawn_thread || {
let a: !*TlsStream = l.attach(fd.val); // handshake off the accept loop
if a.ok {
let s: *TlsStream = a.val;
s.write_all("HTTP/1.1 200 OK\r\n\r\nhi");
s.close();
}
};
}
tls_attach_server
Server-side counterpart to TlsStream::attach: wrap an already-accepted plaintext connection in TLS for protocols that promote inline (SMTP STARTTLS, IMAP STARTTLS, POP3 STLS, FTPS AUTH TLS).
tls_attach_server(conn_fd: i32, cert_path: string, key_path: string) -> !*TlsStream
On success the returned TlsStream owns the underlying socket; do not close the original connection afterwards.
// In an SMTP server, after the client sends STARTTLS:
let r: !*TlsStream = tls_attach_server(conn_fd, "cert.pem", "key.pem");
if !r.ok { eprintln(r.err); return 1; }
let s: *TlsStream = r.val;
defer s.close();
s.write_all("220 ready\r\n"); // rest of the session is encrypted
Low-level sockets — Socket
Beneath the TCP/UDP/TLS conveniences sits a thin, typed socket foundation: stdlib::net::sys declares the runtime's gnet_* syscall surface and wraps an owning fd in Socket, a binary-safe Stream trait, and a flat set of family / option / shutdown constants. stdlib::net::sockopt layers the common option toggles on any .fd; stdlib::net::raw opens SOCK_RAW sockets; and stdlib::net::icmp builds ping / traceroute in pure Glide on top of a raw socket. Every call returns !T and surfaces errors via .ok / .err (no panics), so a missing privilege or an unsupported option is just an error value.
Constants
From stdlib::net::sys (each is pub const … : i32):
// socket type
SOCK_STREAM = 1 SOCK_DGRAM = 2 SOCK_RAW = 3
// address family
AF_V4 = 4 AF_V6 = 6
// shutdown(how)
SHUT_RD = 0 SHUT_WR = 1 SHUT_RDWR = 2
// gnet_const() option tags (resolved to per-OS numbers in netcore.c)
OPT_SOL_SOCKET = 0 OPT_SO_REUSEADDR = 1 OPT_IPPROTO_TCP = 2
OPT_TCP_NODELAY = 3 OPT_SO_BROADCAST = 4 OPT_SO_REUSEPORT = 5
OPT_SO_RCVBUF = 6 OPT_SO_SNDBUF = 7 OPT_IPPROTO_IP = 8
OPT_IP_TTL = 9 OPT_IP_MULTICAST_TTL = 10 OPT_IP_MULTICAST_LOOP = 11
The OPT_* tags are indices into gnet_const, which maps them to the actual platform option numbers — pass them to Socket::set_opt_int. From stdlib::net::raw, IPPROTO_ICMP = 1 is the IANA protocol number for a raw ICMP socket.
Socket
pub struct Socket {
pub fd: i64,
}
A single owning fd. Socket implements Stream, so it is closed through the trait's close (there is no separate inherent close).
Statics and methods:
Socket::open(family: i32, ty: i32, proto: i32) -> !*Socket— open a socket.familyisAF_V4/AF_V6,tyisSOCK_STREAM/SOCK_DGRAM/SOCK_RAW,protois0for the default.bind(self: *Socket, sa: *SocketAddr) -> !i32— bind to a local address.connect(self: *Socket, sa: *SocketAddr) -> !i32— connect to a remote address.listen(self: *Socket, backlog: i32) -> !i32accept(self: *Socket) -> !*Socket— accept one connection, returning a fresh owningSocketfor the peer (reactor-integrated; the accepted socket is left non-blocking +TCP_NODELAY).local_addr(self: *Socket) -> !*SocketAddr— the locally-bound address.peer_addr(self: *Socket) -> !*SocketAddr— the connected peer's address.set_opt_int(self: *Socket, level: i32, opt: i32, val: i32) -> !i32— set an int-valued option;level/optcome fromgnet_const.set_reuse_addr(self: *Socket, on: bool) -> !i32—SO_REUSEADDR.shutdown(self: *Socket, how: i32) -> !i32—howisSHUT_RD/SHUT_WR/SHUT_RDWR.send(self: *Socket, buf: *void, n: i32) -> !i32— sendnbytes; returns bytes sent.recv(self: *Socket, buf: *void, max: i32) -> !i32— receive up tomaxbytes; returns bytes read (0at EOF).
import stdlib::net::sys::*;
import stdlib::net::ip::*;
// Open, bind, listen on a stream socket by hand.
let so = Socket::open(AF_V4, SOCK_STREAM, 0)?;
so.set_reuse_addr(true)?;
let addr = SocketAddr::new(IpAddr::v4(127, 0, 0, 1), 8080);
so.bind(addr)?;
so.listen(128)?;
let where = so.local_addr()?;
println!("listening on", where.to_string()); // => listening on 127.0.0.1:8080
Stream
The binary-safe transport trait. Socket implements it, and TLS/raw streams share the same shape, so generic code can take a *dyn Stream.
pub trait Stream {
fn read_bytes(self: *Self, buf: *u8, max: i32) -> !i32; // bytes read (0 = EOF)
fn write_bytes(self: *Self, buf: *u8, n: i32) -> !i32; // bytes written
fn close(self: *Self);
}
Two free functions wrap a *dyn Stream for string-shaped I/O:
stream_read(s: *dyn Stream, max: i32) -> !string— read up tomaxbytes as a string.stream_write_all(s: *dyn Stream, data: string) -> !i32— write a whole string; returns bytes written.
import stdlib::net::sys::*;
import stdlib::net::ip::*;
let so = Socket::open(AF_V4, SOCK_STREAM, 0)?;
so.connect(SocketAddr::new(IpAddr::v4(93, 184, 216, 34), 80))?;
stream_write_all(so, "GET / HTTP/1.0\r\n\r\n")?;
let head = stream_read(so, 256)?;
println!(head); // => HTTP/1.0 200 OK ...
so.close(); // Stream::close frees the Socket
Socket options — sockopt
stdlib::net::sockopt gives a uniform surface over any foundation fd (a Socket, TcpStream, or UdpSocket — they all expose .fd). Each takes the fd and returns !i32. An option unsupported on the current platform (e.g. SO_REUSEPORT off Linux) returns an error rather than silently succeeding.
sock_set_reuse_addr(fd: i64, on: bool) -> !i32—SO_REUSEADDR, rebind a recently-closed port immediately.sock_set_reuse_port(fd: i64, on: bool) -> !i32—SO_REUSEPORT, load-balance accepts across workers (Linux).sock_set_nodelay(fd: i64, on: bool) -> !i32—TCP_NODELAY, disable Nagle so small writes flush at once.sock_set_broadcast(fd: i64, on: bool) -> !i32—SO_BROADCAST, allow sending to a broadcast address (UDP).sock_set_ttl(fd: i64, ttl: i32) -> !i32—IP_TTL, outgoing unicast hop limit.sock_set_recv_buf(fd: i64, bytes: i32) -> !i32—SO_RCVBUFsize hint.sock_set_send_buf(fd: i64, bytes: i32) -> !i32—SO_SNDBUFsize hint.sock_set_timeout_ms(fd: i64, ms: i32) -> !i32— read + write timeout in ms (0disables); backed bySO_RCVTIMEO/SO_SNDTIMEO. Only bites on a blocking fd.
import stdlib::net::sys::*;
import stdlib::net::sockopt::*;
import stdlib::net::ip::*;
let so = Socket::open(AF_V4, SOCK_STREAM, 0)?;
sock_set_reuse_addr(so.fd, true)?;
sock_set_nodelay(so.fd, true)?;
let r = sock_set_reuse_port(so.fd, true);
if !r.ok { println!("no SO_REUSEPORT here:", r.err); } // off-Linux: prints the error
sock_set_timeout_ms(so.fd, 5000)?; // bound blocking reads at 5s
Raw sockets — RawSocket
stdlib::net::raw opens SOCK_RAW sockets for IP-layer payloads (ICMP, custom protocols, packet inspection). Raw sockets require privilege — root / CAP_NET_RAW on Linux+macOS, Administrator on Windows — so open_* returns an error (typically "1:Operation not permitted") when the process isn't allowed.
pub struct RawSocket {
pub fd: i64,
}
// one received packet (includes the IP header on a SOCK_RAW receive)
pub struct RawRecv {
pub data: string,
pub n: i32,
pub from: *SocketAddr,
}
RawSocket::open_ip(proto: i32) -> !*RawSocket— open a raw IPv4 socket forproto(e.g.IPPROTO_ICMP).RawSocket::open_icmp() -> !*RawSocket— open a raw ICMP socket (the common case).send_to(self: *RawSocket, buf: *void, n: i32, dst: *SocketAddr) -> !i32— sendnbytes todst; the IP layer adds the header. Returns bytes sent.recv_from(self: *RawSocket, buf: *void, max: i32) -> !*RawRecv— receive one packet into a caller-owned buffer; returns the byte count (.n) and sender (.from). Blocking.set_ttl(self: *RawSocket, ttl: i32) -> !i32— outgoing hop limit (used by traceroute).set_timeout_ms(self: *RawSocket, ms: i32) -> !i32— receive timeout in ms (0disables).close(self: *RawSocket)
import stdlib::net::raw::*;
let r: !*RawSocket = RawSocket::open_icmp();
if !r.ok {
println!("need root:", r.err); // => need root: 1:Operation not permitted
return 1;
}
let sock: *RawSocket = r.val;
sock.set_timeout_ms(2000)?;
sock.close();
ICMP — ping & traceroute
stdlib::net::icmp assembles ICMP echo messages byte-by-byte (header + one's-complement checksum) over a raw socket — no C beyond the foundation. Like raw sockets, these need privilege and return err(...) when not permitted.
icmp_checksum(buf: *u8, n: i32) -> i32— the Internet checksum (RFC 1071). Over a complete packet whose checksum field is filled in, returns0(the validity check).icmp_build_echo(buf: *u8, id: i32, seq: i32, payload_len: i32) -> i32— assemble an echo-request intobuf(needs8 + payload_lenbytes); returns the total length.ping(host: string, timeout_ms: i32) -> !i64— pinghostonce; returns the round-trip time in milliseconds.traceroute(host: string, max_hops: i32, timeout_ms: i32) -> !*Vector<string>— probe TTL1..max_hops; one line per hop,"<ttl> <ip>"for a responding router or"<ttl> *"on timeout. Stops at the destination's echo reply. (Intermediate hops are Linux/macOS only; on Windows the routers' time-exceeded replies don't reach a raw socket, so hops read*until the final reply.)
import stdlib::net::icmp::*;
let r: !i64 = ping("example.com", 2000);
if r.ok { println!("rtt:", r.val, "ms"); } // => rtt: 14 ms
else { println!(r.err); } // e.g. icmp: timeout
import stdlib::net::icmp::*;
let tr = traceroute("example.com", 16, 1000)?;
for let i: i32 = 0; i < tr.len(); i++ {
println!(tr.get(i)); // => 1 192.168.0.1
} // 2 10.0.0.1
// 3 *
WebSocket — WebSocket
stdlib::net::ws is an RFC 6455 WebSocket client over plain TCP (ws://) or TLS (wss://). V1 covers the common case: text + binary frames, ping/pong, and clean close. Extensions (permessage-deflate, multiple subprotocols) are out of scope, and inbound payloads are capped to keep buffer bounds tame. There is no server-side accept API yet — this module connects, it does not listen.
import stdlib::net::ws::*;
Every fallible call returns !T, surfaced through .ok / .val / .err. Client frames are always masked, per RFC 6455 §5.3 — the library handles the masking for you.
WsOp — frame opcodes
WsOp is a namespace of opcode constants (RFC 6455 §11.8). Compare them against WsMessage.kind.
| Static | Value | Meaning |
|---|---|---|
WsOp::cont() |
0 |
Continuation frame (fragmented messages) |
WsOp::text() |
1 |
Text frame |
WsOp::binary() |
2 |
Binary frame |
WsOp::close() |
8 |
Close frame (WsMessage.close_code carries the reason) |
WsOp::ping() |
9 |
Ping frame |
WsOp::pong() |
10 |
Pong frame |
let m: !*WsMessage = ws.recv();
if m.ok {
match m.val.kind {
k if k == WsOp::text() => println!("text:", m.val.text),
k if k == WsOp::ping() => { ws.pong(m.val.text); }, // keep-alive
k if k == WsOp::close() => println!("closed:", m.val.close_code),
_ => {},
}
}
WsMessage
One inbound frame, returned by WebSocket::recv.
pub struct WsMessage {
pub kind: i32, // one of WsOp::text() / binary() / close() / ping() / pong()
pub text: string, // payload (text frames carry the string; binary reuses this field)
pub close_code: i32, // populated when kind == WsOp::close(), else 0
}
There is no dedicated bytes type yet, so binary payloads also arrive in text.
WsParsedUrl
The parsed form of a ws:// / wss:// URL.
pub struct WsParsedUrl {
pub is_wss: bool, // true for wss://, false for ws://
pub host: string, // host without port
pub port: i32, // explicit port, else 80 (ws) / 443 (wss)
pub path: string, // request path, defaults to "/"
}
host_header(self) -> string
Format the host for the HTTP Host header, dropping the port when it matches the scheme default (80 for ws, 443 for wss).
// (WsParsedUrl values come from WebSocket::connect internally; shown for shape)
// host_header() => "example.com:8443" when a non-default port is set,
// => "example.com" when the port is the scheme default
WebSocket
The client connection. Obtain one with WebSocket::connect; it owns a *dyn Stream over either a TcpStream (ws://) or a TlsStream (wss://), so no transport branch leaks into your code.
WebSocket::connect(url) -> !*WebSocket
Open a connection and perform the HTTP Upgrade handshake. url must be ws://host[:port]/path or wss://host[:port]/path. The call verifies the server returned 101 Switching Protocols and that Sec-WebSocket-Accept matches base64(sha1(key + magic)); any mismatch returns an err.
let r: !*WebSocket = WebSocket::connect("wss://echo.example.com/sock");
if !r.ok { println!(r.err); return 1; }
let ws: *WebSocket = r.val;
ws.send_text("hello");
let m: !*WsMessage = ws.recv();
if m.ok { println!("got:", m.val.text); } // => got: hello
ws.close(1000, "bye");
send_text(self, msg) -> !i32
Send a UTF-8 text frame (opcode 1). Always masked.
let w: !i32 = ws.send_text("{\"type\":\"hello\"}");
if !w.ok { println!("send failed:", w.err); }
send_binary(self, data) -> !i32
Send a binary frame (opcode 2). Payload bytes live in a string.
ws.send_binary("\x00\x01\x02\x03");
pong(self, payload) -> !i32
Send a Pong (opcode 10), typically echoing a received Ping payload to keep the connection alive.
let m: !*WsMessage = ws.recv();
if m.ok && m.val.kind == WsOp::ping() {
ws.pong(m.val.text); // echo the ping payload back
}
recv(self) -> !*WsMessage
Read one inbound frame. Pings are returned verbatim — the caller decides whether to auto-pong. Close frames surface with kind == WsOp::close() and close_code populated. Masked payloads are unmasked for you.
let m: !*WsMessage = ws.recv();
if !m.ok { println!("recv error:", m.err); return 1; }
match m.val.kind {
k if k == WsOp::text() => println!(m.val.text),
k if k == WsOp::close() => println!("closed:", m.val.close_code),
_ => {},
}
close(self, code, reason) -> ()
Send a close frame and tear down the transport. code 1000 is "normal closure"; see RFC 6455 §7.4 for the registry. This frees the connection — do not use ws afterward.
ws.close(1000, "bye"); // normal closure
ws.close(4000, "app reset"); // app-defined code (4000–4999)
Full client: connect, send, recv loop, close
A complete echo-client driver. It connects, sends a message, then loops on recv, auto-ponging pings and breaking on a close frame.
import stdlib::net::ws::*;
fn run() -> i32 {
let r: !*WebSocket = WebSocket::connect("wss://echo.example.com/sock");
if !r.ok { println!("connect:", r.err); return 1; }
let ws: *WebSocket = r.val;
let s: !i32 = ws.send_text("ping me");
if !s.ok { println!("send:", s.err); ws.close(1011, "send failed"); return 1; }
for let i: i32 = 0; i < 8; i++ {
let m: !*WsMessage = ws.recv();
if !m.ok { println!("recv:", m.err); break; }
let msg: *WsMessage = m.val;
match msg.kind {
k if k == WsOp::text() => println!("text:", msg.text),
k if k == WsOp::binary() => println!("binary bytes:", msg.text.len()),
k if k == WsOp::ping() => { ws.pong(msg.text); },
k if k == WsOp::close() => { println!("closed:", msg.close_code); break; },
_ => {},
}
}
ws.close(1000, "done");
return 0;
}
HTTP client — HttpClient
A blocking HTTP/1.1 and HTTP/2 client. HttpClient holds reusable config (user-agent, redirect policy, cookie jar, timeout, TLS verification, HTTP/2 opt-in); HttpClientRequest is the mutable request envelope you build and hand to it. Both http:// and https:// are supported — HTTPS rides on stdlib::net::tls::TlsStream, and when http2 is enabled the client tries h2 over ALPN and transparently falls back to HTTP/1.1.
Every call returns !*HttpResponse (a Result): test .ok, read the value from .val, the message from .err. The response type is HttpResponse from stdlib::http — its accessors (.status, .body, .header(name), .headers(name)) are documented in the HTTP server section.
import stdlib::http::client::*;
let r: !*HttpResponse = http_get("http://example.com/");
if r.ok { println!(r.val.status, r.val.body.len(), "bytes"); }
HttpClientRequest
The request being assembled. A plain mutable holder — distinct from the server-side HttpRequest, which is parsed from the wire.
pub struct HttpClientRequest {
pub method: string,
pub url: string,
pub headers: string, // raw "Name: Value\r\n" lines
pub body: string,
}
pub fn new(method: string, url: string) -> *HttpClientRequest— build anpub fn set(self, name: string, value: string)— append one header line.pub fn body_str(self, s: string)— set the body and the matching
envelope with empty headers and body.
Multiple calls add multiple lines; there is no dedup, so last-wins is the caller's responsibility.
Content-Length header in one step.
let req: *HttpClientRequest = HttpClientRequest::new("PATCH", "https://api.example.com/me");
req.set("Authorization", "Bearer xyz");
req.set("Content-Type", "application/json");
req.body_str("{\"name\":\"new\"}");
let r: !*HttpResponse = HttpClient::new().do(req);
if r.ok { println!(r.val.status); } // => 200
HttpClient
Persistent client config. Keep one around to share defaults across many requests; for one-shot calls the http_get / http_post_json free functions build a default client per call.
pub struct HttpClient {
pub user_agent: string, // sent as User-Agent; default "glide/0.1"
pub max_redirects: i32, // hops `do` will follow; default 5
pub jar: *CookieJar, // null = no cookie handling
pub tls_insecure: bool, // skip cert verification (dev only); default false
pub timeout_ms: i32, // per-request total timeout; 0 = disabled
pub http2: bool, // try h2 over TLS via ALPN; default false
}
Field semantics:
user_agent— value of theUser-Agentrequest header.max_redirects— maximum redirect hopsdofollows before returningjar— assign aCookieJar::new()to enable automaticSet-Cookietls_insecure— whentrue, skip TLS certificate validation. Local devtimeout_ms— per-request total timeout in milliseconds (connect + send +http2— try HTTP/2 over TLS via ALPN, falling back to HTTP/1.1 when the
err("http_client: too many redirects"). do_once ignores this.
ingestion plus Cookie: header attachment across requests. Stays null (no cookie handling) by default.
only; production code MUST leave this off.
receive), applied via socket send/receive timeouts. 0 disables it.
server doesn't negotiate h2. No effect on http:// URLs (h2c upgrade is not implemented).
Methods:
pub fn new() -> *HttpClient— construct with the defaults above.pub fn do(self, req: *HttpClientRequest) -> !*HttpResponse— send andpub fn do_once(self, req: *HttpClientRequest) -> !*HttpResponse— sendpub fn get(self, url: string) -> !*HttpResponse— one-shot GET viado.pub fn post(self, url: string, body: string, content_type: string) -> !*HttpResponsepub fn post_json(self, url: string, body: string) -> !*HttpResponse—pub fn put(self, url: string, body: string, content_type: string) -> !*HttpResponsepub fn delete(self, url: string) -> !*HttpResponse— DELETE.
follow redirects up to max_redirects. A 303 demotes the method to GET and drops the body; 301/302 do the same for non-GET/HEAD methods; 307/308 preserve method and body. Relative Location values are resolved against the current URL.
exactly one request, never following redirects. Use for proxies and crawlers that must preserve 30x responses verbatim.
— POST with an explicit Content-Type.
POST with Content-Type: application/json. The caller serialises the JSON.
— PUT with an explicit Content-Type.
Configured client with custom headers and explicit error handling:
let c: *HttpClient = HttpClient::new();
c.user_agent = "myapp/1.0";
let req: *HttpClientRequest = HttpClientRequest::new("GET", "https://api.example.com/health");
req.set("Accept", "application/json");
let r: !*HttpResponse = c.do(req);
if !r.ok {
println!("request failed:", r.err);
return;
}
let resp: *HttpResponse = r.val;
println!("status:", resp.status); // => 200
println!("type:", resp.header("Content-Type")); // => application/json
POST JSON and read status, a header, and the body of the response:
let c: *HttpClient = HttpClient::new();
let r: !*HttpResponse = c.post_json("https://api.example.com/users",
"{\"name\":\"alice\"}");
if r.ok {
let resp: *HttpResponse = r.val;
println!(resp.status); // => 201
println!(resp.header("Location")); // => /users/7
println!(resp.body); // => {"id":7,"name":"alice"}
}
Following redirects with a timeout and a cookie jar. Assigning a jar captures every Set-Cookie and replays the matching Cookie: header on later requests; timeout_ms bounds each hop; max_redirects caps the chain:
import stdlib::http::cookies::*;
let c: *HttpClient = HttpClient::new();
c.jar = CookieJar::new(); // persist cookies across requests
c.timeout_ms = 5000; // 5s per request
c.max_redirects = 10; // follow up to 10 hops
let login: !*HttpResponse = c.post("https://example.com/login",
"user=a&pass=b",
"application/x-www-form-urlencoded");
if !login.ok { println!(login.err); return; }
// The session cookie set above is now sent automatically and any 30x
// redirect from /dashboard is followed transparently.
let page: !*HttpResponse = c.get("https://example.com/dashboard");
if page.ok { println!(page.val.status); } // => 200
To inspect a redirect instead of following it, use do_once:
let req: *HttpClientRequest = HttpClientRequest::new("GET", "https://example.com/old");
let r: !*HttpResponse = HttpClient::new().do_once(req);
if r.ok && r.val.status == 301 {
println!("redirects to", r.val.header("Location"));
}
HTTP/2
Set http2 = true to negotiate h2 over ALPN on HTTPS connections. The API is unchanged — responses come back as the same HttpResponse shape — and the client silently falls back to HTTP/1.1 when the server declines h2:
let c: *HttpClient = HttpClient::new();
c.http2 = true;
let r: !*HttpResponse = c.get("https://example.com/");
if r.ok { println!(r.val.status); } // => 200 (over h2 if offered)
ParsedUrl
The parsed form of a request URL. Produced internally by the client; exposed for callers that need its Host:-header formatting.
pub struct ParsedUrl {
pub scheme: string, // "http" or "https"
pub host: string,
pub port: i32, // defaults to 80 (http) / 443 (https)
pub path: string, // always starts with "/"
}
pub fn host_header(self) -> string— format theHost:header value,
omitting the port when it is the scheme default (80 for http, 443 for https) and keeping it otherwise (e.g. example.com:8443).
Free functions
One-shot helpers that build a default HttpClient per call. For multiple requests or shared state (cookies, timeouts), construct HttpClient::new() once and reuse it instead.
pub fn http_get(url: string) -> !*HttpResponse— GET with a default client.pub fn http_post_json(url: string, body: string) -> !*HttpResponse— JSON
POST with a default client.
let r: !*HttpResponse = http_post_json("https://api.example.com/login",
"{\"user\":\"a\",\"pass\":\"b\"}");
if r.ok && r.val.status == 200 {
println!(r.val.body); // => {"token":"..."}
}
HTTP server — HttpResponse
A minimal HTTP/1.1 server built on the async Listener / Conn pair. A handler is a plain fn(*HttpRequest) -> HttpResponse; the entry points take a port and that handler. On Linux each connection runs on a parked coroutine so one process holds tens of thousands of clients; on Windows / macOS / BSD the loop is serial (one connection at a time) until IOCP / kqueue reactors land.
import stdlib::http::*;
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("hello, ".concat(req.path));
}
fn main() -> i32 {
http_listen(8080, root); // never returns on success
return 0;
}
Limitations of v1: HTTP/1.1 only on this path (HTTP/2 and HTTP/3 have their own entry points), one handler for every path (routing lives on top — see the router section), and the body is fully buffered before the handler runs (no streaming uploads).
HttpRequest
One incoming request, fully buffered. The pointer's lifetime is the handler call — do not stash it.
pub struct HttpRequest {
pub method: string,
pub path: string,
pub version: string,
pub headers_block: string, // raw "Name: Value\r\n…" wire text
pub body: string,
pub params: *HashMap<string>, // router path params
pub queries: *HashMap<string>, // lazily parsed query map
pub tls: bool, // true if arrived via https_listen
pub headers_cache: *HashMap<string>, // lazy header lookup cache
}
Getters:
header(self: *HttpRequest, name: string) -> string— case-insensitiveparam(self: *HttpRequest, name: string) -> string— router pathquery(self: *HttpRequest, name: string) -> string— decodedpath_only(self: *HttpRequest) -> string—pathwith the?...is_method(self: *HttpRequest, m: string) -> bool— case-insensitivebody_json(self: *HttpRequest) -> !*JsonValue— parse the body as JSON;
header lookup, "" when missing. First call parses headers_block into a cache; later calls are O(1). Repeated headers keep the first value.
parameter, "" when unknown or dispatched without a router.
query-string value, "" when missing. Parsed + cached on first access.
query segment stripped.
method check.
err on an empty or invalid body.
fn handler(req: *HttpRequest) -> HttpResponse {
if !req.is_method("POST") {
return HttpResponse::with_status(405).text("method not allowed");
}
let host: string = req.header("Host"); // e.g. "localhost:8080"
let q: string = req.query("q"); // "" when no ?q=
let v: !*JsonValue = req.body_json();
if !v.ok { return HttpResponse::with_status(400).text(v.err); }
let name: !string = v.val.get_string("name");
if !name.ok { return HttpResponse::with_status(400).text(name.err); }
return HttpResponse::ok().text("hi ".concat(name.val).concat(" @ ").concat(host));
}
HttpResponse
The outgoing response. Build with a constructor, then chain the setters — every setter returns a fresh HttpResponse (value semantics, not a mutating pointer).
pub struct HttpResponse {
pub status: i32,
pub headers_block: string,
pub body: string,
pub stream_src: *dyn ChunkSource, // null for buffered responses
pub file_path: string, // non-empty ⇒ sendfile path
}
Constructors (static):
HttpResponse::with_status(code: i32) -> HttpResponse— given status, noHttpResponse::ok() -> HttpResponse— 200, no body.HttpResponse::not_found() -> HttpResponse— 404.HttpResponse::redirect(url: string) -> HttpResponse— 302 withHttpResponse::file(path: string, mime: string) -> HttpResponse— serve
body, no headers.
Location: url.
a file from disk via zero-copy sendfile; Content-Length is the file size, mime fills Content-Type.
Body + content-type setters (each chainable, returns HttpResponse):
body(self, s: string)— set body and matchingContent-Length.text(self, s: string)— body +Content-Type: text/plain; charset=utf-8.html(self, s: string)— body +Content-Type: text/html; charset=utf-8.json(self, s: string)— body +Content-Type: application/json.json_of(self, v: *JsonValue)— serialisev.emit()as JSON.stream(self, source: *dyn ChunkSource)— emit
Transfer-Encoding: chunked, walking source.next_chunk() until none(); body is ignored once a stream source is set.
Header + status setters:
status(self, code: i32)— replace the status code.set(self, name, value: string)— set a header, replacing any existingadd(self, name, value: string)— append a header line, preservingcookie(self, name, value: string)— append aSet-Cookie: name=value
same-name line (case-insensitive). CR/LF in name/value is stripped.
same-name lines (for Set-Cookie, Vary, Link).
header (routes through add).
Header getters (take *HttpResponse):
header(self: *HttpResponse, name: string) -> string—headers(self: *HttpResponse, name: string) -> *Vector<string>— every
case-insensitive lookup, "" when missing.
matching header line (for Set-Cookie).
let r: HttpResponse = HttpResponse::ok()
.set("X-App", "demo")
.cookie("session", "abc123")
.cookie("csrf", "xyz")
.text("ready");
r.header("X-App"); // => "demo"
r.headers("Set-Cookie").len(); // => 2
A JSON-returning handler, built two ways:
fn api(req: *HttpRequest) -> HttpResponse {
// 1. hand-written JSON string
if req.path_only().eq("/ping") {
return HttpResponse::ok().json("{\"ok\":true}");
}
// 2. assembled via JsonValue, serialised by json_of
let v: *JsonValue = JsonValue::object();
v.obj_set("path", JsonValue::string(req.path));
v.obj_set("ok", JsonValue::bool(true));
return HttpResponse::ok().json_of(v);
}
ChunkSource — streaming bodies
A response can stream its body chunk-by-chunk instead of buffering it. Implement the ChunkSource trait on a struct that holds the cursor state; the server emits one chunked frame per next_chunk() and ends on none().
pub trait ChunkSource {
fn next_chunk(self: *Self) -> ?string;
}
pub struct Lines { pub items: *Vector<string>, pub idx: i32 }
impl ChunkSource for Lines {
fn next_chunk(self: *Lines) -> ?string {
if self.idx >= self.items.len() { return none(); }
let s: string = self.items.get(self.idx);
self.idx = self.idx + 1;
return some(s.concat("\n"));
}
}
fn handler(req: *HttpRequest) -> HttpResponse {
let src: *Lines = malloc(sizeof(Lines)) as *Lines;
src.items = ...; src.idx = 0;
return HttpResponse::ok().stream(src as *dyn ChunkSource);
}
Entry points
http_listen(port: i32, handler: fn(*HttpRequest) -> HttpResponse) -> !i32 binds, prints a listening line, and loops forever. Returns err("bind failed") when the port cannot be bound; never returns on success.
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("echo: ".concat(req.path));
}
fn main() -> i32 {
let r: !i32 = http_listen(8080, root);
if !r.ok { eprintln(r.err); return 1; }
return 0;
}
https_listen(port: i32, cert_path: string, key_path: string, handler) -> !i32 is the TLS sibling. cert_path / key_path are PEM files. It advertises h2,http/1.1 over ALPN — h2-capable clients negotiate HTTP/2 transparently, everyone else falls back to HTTP/1.1. req.tls is true inside the handler.
fn main() -> i32 {
let r: !i32 = https_listen(8443, "cert.pem", "key.pem", root);
if !r.ok { eprintln(r.err); return 1; }
return 0;
}
Multi-worker variants
http_listen_workers(port, n, handler) -> !i32 is the standard fast path: n pthread workers, each its own epoll loop + C state machine, with the handler invoked inline on the worker thread (no coroutine spawn). On the reference 4-core box it reaches ~159k req/s (~84% of Axum/Tokio on hello-world). Handlers must be non-blocking — no chan send/recv, no sleep_ms, no parking I/O; the runtime aborts with a message pointing at the blocking variant otherwise. Linux only for the fast path; other platforms route automatically to the blocking variant.
http_listen_workers_blocking(port, n, handler) -> !i32 spawns a coro per connection, so blocking ops inside the handler (DB queries, downstream HTTP, channel sync) park only that connection's coro. ~107k req/s on the same box, unrestricted handlers.
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("hello, ".concat(req.path));
}
fn main() -> i32 {
http_listen_workers(8080, 4, root); // 4 workers, fast path
return 0;
}
@leaf state-machine path
For the hottest endpoints there is a coroutine-free path where the handler runs inline on the worker's epoll thread. The handler signature is fn(string, string) -> string (method, path → body) and must be marked @leaf (no parking — the runtime aborts if it parks). The framing wraps the returned body in a 200 OK text/plain response. ~70% faster than http_listen_workers on hello-world; Linux only.
http_listen_sm_workers(port, n, h: fn(string, string) -> string) -> !i32 is the user-handler variant:
@leaf
fn hello(method: string, _path: string) -> string {
return "hello\n";
}
fn main() -> i32 {
http_listen_sm_workers(8080, 4, hello);
return 0;
}
Two bench baselines round out the family (no user handler — they always reply with a fixed hello-world body):
http_listen_sm_hello(port: i32) -> !i32— single-worker.http_listen_sm_hello_workers(port: i32, n: i32) -> !i32—nworkers,
each with its own SO_REUSEPORT listener.
Lower-level helpers
parse_http_request(raw: string) -> !HttpRequest— parse a completeformat_http_response(r: *HttpResponse) -> string— render a response
HTTP/1.1 request from raw bytes; err on a malformed or partial request.
to wire bytes (always Connection: close). Exposed so responses can be piped to a logger or recorder.
let r: !HttpRequest = parse_http_request("GET /x HTTP/1.1\r\nHost: a\r\n\r\n");
if r.ok { println!(r.val.method, r.val.path); } // GET /x
let bytes: string = format_http_response(&HttpResponse::ok().body("hi"));
Routing & handlers — Router
Router matches an incoming request's method + URL path against a table of patterns declared up front and dispatches the first match. Patterns support three segment shapes:
/users— literal, must match byte-for-byte./users/:id— param, captures one segment, read viareq.param("id")./static/*rest— wildcard, captures the rest of the path (zero or more
segments) into req.param("rest"). A wildcard may only appear last.
Method dispatch is exact (GET ≠ POST) and case-insensitive on registration; register the same pattern twice for two verbs, or use any(...) for a catch-all.
Import both the http types and the router:
import stdlib::http::*;
import stdlib::http::router::*;
Segment-kind constants
pub const RS_LITERAL: i32 = 0; // literal text segment
pub const RS_PARAM: i32 = 1; // `:name` capture
pub const RS_WILDCARD: i32 = 2; // `*name` rest-of-path capture
pub const ANY_METHOD: string = "*"; // sentinel verb for `any(...)`
RouteSegment
One compiled piece of a route pattern.
pub struct RouteSegment {
pub kind: i32, // RS_LITERAL | RS_PARAM | RS_WILDCARD
pub text: string, // literal text, or the capture name for param/wildcard
}
Route
One row of the routing table.
pub struct Route {
pub method: string,
pub pattern: string,
pub segments: *Vector<RouteSegment>,
pub handler: fn(*HttpRequest) -> HttpResponse,
pub local_mws: *Vector<fn(*HttpRequest, *Chain) -> HttpResponse>,
}
local_mws is the per-route middleware chain, populated when the route was mounted through Router::scope(prefix, sub). Parent middleware runs first, then local_mws in registration order, then the handler.
Router
pub struct Router {
pub routes: *Vector<Route>,
pub mw: *Vector<fn(*HttpRequest, *Chain) -> HttpResponse>,
pub not_found: fn(*HttpRequest) -> HttpResponse,
}
Routes are stored in declaration order; the first match wins. mw runs before the matched handler in registration order. not_found covers a no-match dispatch and defaults to a plain 404 with body "not found".
Construction & registration
Router::new() -> *Router— empty router with the default 404 handler.route(self, method, pattern, handler)— registerhandlerforget / post / put / delete / patch / head / options (self, pattern, handler)any(self, pattern, handler)— match every HTTP method onpattern.route_with(self, method, pattern, mws, handler)— likeroutebut with anot_found_with(self, handler)— override the default 404 handler.free(self)— release the route table and the router struct; pair with
method + pattern. The method is stored upper-case; pass ANY_METHOD (or "*") for every verb.
— verb shortcuts for route(...).
per-route middleware list (router-level mw runs first, then mws, then the handler).
defer.
A handler has the shape fn(*HttpRequest) -> HttpResponse. Read captured params with req.param("name"):
import stdlib::http::*;
import stdlib::http::router::*;
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().text("hi");
}
fn get_user(req: *HttpRequest) -> HttpResponse {
let id: string = req.param("id"); // captured from `:id`
return HttpResponse::ok().json("{\"id\":\"".concat(id).concat("\"}"));
}
fn create_user(req: *HttpRequest) -> HttpResponse {
return HttpResponse::with_status(201).json(req.body);
}
fn main() -> i32 {
let r: *Router = Router::new();
defer r.free();
r.get("/", root);
r.get("/users/:id", get_user);
r.post("/users", create_user);
return r.listen(8080).val;
}
A wildcard route captures everything after the prefix:
fn serve_static(req: *HttpRequest) -> HttpResponse {
let rest: string = req.param("rest"); // `/static/css/app.css` -> "css/app.css"
return HttpResponse::ok().text(rest);
}
let r: *Router = Router::new();
r.get("/static/*rest", serve_static);
r.any("/health", fn(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().text("ok"); // matches GET/POST/HEAD/...
});
dispatch rejects any path containing a .. segment with a 400 bad path before consulting the table, so a wildcard file-server handler can't read outside its root.
dispatch
dispatch(self, req) -> HttpResponse walks the table, and on the first match populates req.params, runs the middleware chain, then calls the handler. With no match it runs the not-found handler. It mutates req.params in place, so pass a freshly built request.
let req: HttpRequest = ...;
let resp: HttpResponse = r.dispatch(&req); // => handler's HttpResponse
Application state
state(self, p: *void) -> *Router stores an application-state pointer process-wide. Handlers reach it through the State<T> extractor without closure capture. v1 has a single global slot. Returns self for chaining.
let r: *Router = Router::new();
r.state(db as *void); // db: *Db
r.get("/users/:id", get_user);
Serving
listen(self, port) -> !i32— coroutine-per-conn loop. On Linux uses thelisten_workers(self, port, n) -> !i32— routes through the Clisten_workers_blocking(self, port, n) -> !i32— coroutine-per-conn
reactor + per-conn coroutines; Windows / macOS / BSD run serially until their reactors land.
state-machine HTTP server for max throughput. Handlers MUST be non-blocking (no chan.send/recv, no sleep_ms, no parking I/O).
across n workers (SO_REUSEPORT load-balanced where supported) for handlers that genuinely need to block.
All three return !i32; a bind failure surfaces as the err arm:
let r: *Router = Router::new();
r.get("/", root);
let res: !i32 = r.listen(8080);
if !res.ok { println!("listen failed: ", res.err); }
Middleware & route groups — Chain
Middleware has the shape fn(*HttpRequest, *Chain) -> HttpResponse. Each one either calls chain_next(c) to advance, or returns without calling it to short-circuit (the rest of the chain and the route handler are skipped).
pub struct Chain {
pub idx: i32,
pub mws: *Vector<fn(*HttpRequest, *Chain) -> HttpResponse>,
pub handler: fn(*HttpRequest) -> HttpResponse,
pub req: *HttpRequest,
}
pub fn chain_next(c: *Chain) -> HttpResponse
chain_next(c) runs the next middleware, or the route handler once the list is exhausted. The returned response may be inspected or mutated before the middleware returns it ("after"-style behaviour).
use_mw(self, mw) -> *Router— register a router-level middleware; runsscope(self, prefix, sub) -> *Router— mount every route ofsubunder
before the handler in registration order. Returns self for chaining.
prefix. The sub-router's mw becomes per-route middleware that runs after the parent's chain and before each handler. Copies the route entries, so sub may be dropped or reused afterwards. Returns self.
fn logger(req: *HttpRequest, c: *Chain) -> HttpResponse {
println!("[", req.method, "] ", req.path);
let resp: HttpResponse = chain_next(c);
println!(" -> ", resp.status);
return resp;
}
fn auth(req: *HttpRequest, c: *Chain) -> HttpResponse {
if req.header("Authorization").len() == 0 {
return HttpResponse::with_status(401).text("no auth"); // short-circuit
}
return chain_next(c);
}
let api: *Router = Router::new();
api.use_mw(auth);
api.get("/users/:id", get_user);
let r: *Router = Router::new();
r.use_mw(logger);
r.scope("/api/v1", api);
// GET /api/v1/users/42 runs: logger -> auth -> get_user
r.listen(8080);
Typed handlers — the @handler proc-macro
@handler gives a fn typed-extractor params and a flexible return, then emits a fn(*HttpRequest) -> HttpResponse wrapper the router accepts unchanged. Param dispatch, in order:
req: *HttpRequest— passthrough, no extraction.Json<T>—req.body_json()+T::from_json(400 on parse, 422 on bind).Path<T>—Path::extract(req, "<param-name>"); the param's ident is theQuery<T>—Query::extract(req, "<param-name>")against the query string.State<T>—State::extract(req), reading the slot set byRouter::state.- any other
T—T::from_request(req)via theFromRequesttrait.
path key.
Return shape: HttpResponse passes through; Json<T> is auto-wrapped via .into_response().
@handler
fn show(id: Path<i32>) -> Json<User> {
return Json::wrap(load_user(id.val)); // auto .into_response()
}
let r: *Router = Router::new();
r.state(db as *void);
r.get("/users/:id", show); // wrapper has the fn-ptr shape
Attribute routing — @get/@post/… + routes!
The route attrs (@route(METHOD, "/path") plus sugar @get, @post, @put, @delete, @patch, @head, @options, @any) wrap the fn the same way @handler does and emit a _route_register_<name>(r: *Router) function. routes!(r) walks the program and emits the matching r.<method>(...) registration for every attributed fn. The path's :param and *wildcard names are checked at compile time against the handler's Path<T> params (unless the handler takes a single req: *HttpRequest, which opts out).
import stdlib::http::*;
import stdlib::http::router::*;
@get("/users/:id")
fn get_user(id: Path<i32>) -> Json<User> {
return Json::wrap(load_user(id.val));
}
@post("/users")
fn create_user(body: Json<NewUser>) -> Json<User> {
return Json::wrap(insert_user(body.val)); // 422 if the body fails to bind
}
fn main() -> i32 {
let r: *Router = Router::new();
defer r.free();
routes!(r); // registers get_user + create_user
return r.listen(8080).val;
}
@middleware(a, b) stacks route-scoped middleware that the generated registration threads through route_with, so the global use_mw chain is preserved and a, b run after it. The @listen(port) / @listen_workers(port, n) attribute on fn main builds the router, runs routes!, and calls listen / listen_workers for you.
Request binding — FromRequest
Handler params can be typed extractors instead of a raw *HttpRequest. Any type that implements FromRequest (or, for the generic wrappers Json<T> / Authorization<S> / Path<T> / Query<T> / State<T>, exposes an inherent from_request/extract) can sit in a @handler signature; the macro pulls it out of the request, and on failure turns the err string into a response.
Extractors encode the desired HTTP status as a "<code>:<message>" prefix on their err string (e.g. "401:missing token"). HttpResponse::from_extract_err parses that prefix; a plain (unprefixed) err defaults to 422.
The FromRequest trait
pub trait FromRequest {
fn from_request(req: *HttpRequest) -> !Self;
}
Implement it once and the type is usable as a handler param. The err string carries the status code as a prefix.
import stdlib::http::*;
import stdlib::http::extract::*;
pub struct ApiKey { pub key: string }
impl FromRequest for ApiKey {
fn from_request(req: *HttpRequest) -> !ApiKey {
let v: string = req.header("X-Api-Key");
if v.len() == 0 { return err("401:missing X-Api-Key"); }
return ok(ApiKey { key: v });
}
}
// Call it directly, or let @handler do it:
let r: !ApiKey = ApiKey::from_request(req);
if !r.ok { return HttpResponse::from_extract_err(r.err); } // 401 response
let key: string = r.val.key;
*HttpRequest itself implements FromRequest as the identity passthrough, so a handler can still take the raw request.
Bearer — Authorization: Bearer <token>
pub struct Bearer {
pub token: string,
}
Strips the Bearer prefix; errs 401 when the header is missing or uses another scheme.
@handler
fn protected(auth: Bearer) -> HttpResponse {
return HttpResponse::ok().text("token=".concat(auth.token));
}
Headers — full header bag
pub struct Headers {
pub req: *HttpRequest,
}
get(self: *Headers, name: string) -> string— case-insensitive lookup;""when missing.has(self: *Headers, name: string) -> bool—truewhen the header is present and non-empty.
@handler
fn echo_ua(headers: Headers) -> HttpResponse {
if !headers.has("User-Agent") { return HttpResponse::with_status(400); }
return HttpResponse::ok().text(headers.get("User-Agent"));
}
Authorization<S> — generic auth scheme
pub trait AuthScheme {
fn parse(value: string) -> !Self;
fn name() -> string;
}
pub struct Authorization<S> {
pub scheme: S,
}
Authorization<S: AuthScheme> reads the Authorization header (missing → 401) and hands the raw value (prefix included) to S::parse. The static extractor is Authorization::extract-shaped via the inherent method:
from_request(req: *HttpRequest) -> !Authorization<S>
Bearer and Basic both implement AuthScheme, so Authorization<Bearer> and Authorization<Basic> work out of the box.
@handler
fn whoami(auth: Authorization<Bearer>) -> HttpResponse {
return HttpResponse::ok().text(auth.scheme.token);
}
Basic — user + password
pub struct Basic {
pub user: string,
pub pass: string,
}
Basic implements AuthScheme. Note: v1 does not base64-decode — it treats the value after Basic as the literal user:pass, splitting on the first :.
let r: !Basic = Basic::parse("Basic alice:s3cret");
if r.ok {
// r.val.user => "alice", r.val.pass => "s3cret"
}
State<T> — shared application state
pub struct State<T> {
pub val: *T,
}
State registered with Router::state(p) is reachable without globals. The extractor casts the slot to *T; an empty slot errs 500.
extract(req: *HttpRequest) -> !State<T>
let db: *Db = open_db();
let r: *Router = Router::new();
r.state(db as *void);
@handler
fn list_users(state: State<Db>) -> HttpResponse {
return HttpResponse::ok().text(state.val.summary());
}
Path<T> and Query<T> — typed scalars
pub trait FromPath {
fn from_path(s: string) -> !Self;
}
pub struct Path<T> {
pub val: T,
}
pub struct Query<T> {
pub val: T,
}
FromPath is implemented for string, i32, and bool. The @handler macro uses the param ident as the lookup key: Path<T> reads req.param(name) (missing → 404), Query<T> reads req.query(name) (missing → 400); both convert via T::from_path (bad value → 400).
Path::extract(req: *HttpRequest, name: string) -> !Path<T>Query::extract(req: *HttpRequest, name: string) -> !Query<T>
// route /users/:id • GET /users/7?page=2
@handler
fn get_user(id: Path<i32>, page: Query<i32>) -> HttpResponse {
return HttpResponse::ok().text(
"user ".concat(id.val.to_string()).concat(" page ").concat(page.val.to_string()));
}
Calling an extractor by hand:
let r: !Path<i32> = Path::extract(req, "id");
if !r.ok { return HttpResponse::from_extract_err(r.err); } // 404 or 400
let id: i32 = r.val.val;
Form — urlencoded body extractor
pub struct Form {
pub data: *FormData,
}
Extracts an application/x-www-form-urlencoded POST body. Errs 415 when the Content-Type doesn't match, 400 when the body fails to parse.
get(self: *Form, name: string) -> string— first value forname;""when absent.has(self: *Form, name: string) -> bool—truewhen at least one field matches.
@handler
fn submit(form: Form) -> HttpResponse {
if !form.has("user") { return HttpResponse::with_status(400); }
return HttpResponse::ok().text("welcome, ".concat(form.get("user")));
}
Cookies — request cookie bag
pub struct Cookies {
pub header: string,
}
Wraps the Cookie: request header (read-only).
get(self: *Cookies, name: string) -> string— value forname;""when absent.has(self: *Cookies, name: string) -> bool—truewhen present.
@handler
fn me(cookies: Cookies) -> HttpResponse {
let sid: string = cookies.get("session");
if sid.eq("") { return HttpResponse::with_status(401).text("login"); }
return HttpResponse::ok().text("session=".concat(sid));
}
MultipartForm — incoming multipart/form-data
pub struct UploadedFile {
pub name: string,
pub filename: string,
pub content_type: string,
pub body: string,
}
pub struct MultipartForm {
pub fields: *HashMap<string>,
pub files: *Vector<UploadedFile>,
}
MultipartForm implements FromRequest: it checks the Content-Type is multipart/form-data, reads the boundary=, and splits the body into plain fields and uploaded files. Errs 400 on not multipart / no boundary / malformed.
@handler
fn upload(mp: MultipartForm) -> HttpResponse {
if mp.files.len() > 0 {
let f: UploadedFile = mp.files.get(0);
return HttpResponse::ok().text(
f.filename.concat(": ").concat(f.body.len().to_string()).concat(" bytes"));
}
return HttpResponse::ok().text("name=".concat(mp.fields.get("user")));
}
HttpResponse::from_extract_err
pub fn from_extract_err(err_str: string) -> HttpResponse
Turns an extractor's err string into a response. A "<code>:<message>" prefix sets the status and uses the message as the body; an unprefixed string defaults to 422.
let r: !Bearer = Bearer::from_request(req);
if !r.ok { return HttpResponse::from_extract_err(r.err); }
Typed JSON binding — Json<T>
Json<T> carries a typed value to and from a JSON response. The value's type must implement JsonBind (from_json / to_json).
Json<T> and json_respond
pub struct Json<T> {
pub val: T,
}
Json::wrap(v: T) -> Json<T>— wrap aT: JsonBindvalue.json_respond<T: JsonBind>(v: T) -> HttpResponse— one-liner equivalent toJson::wrap(v).into_response(); emits a200with the JSON body andContent-Type: application/json.
import stdlib::http::*;
import stdlib::http::typed::*;
import stdlib::json::*;
pub struct StructLegal {
pub nome: string,
pub idade: ?i32,
}
impl JsonBind for StructLegal {
fn from_json(v: *JsonValue) -> !StructLegal { /* ... */ }
fn to_json(self: *StructLegal) -> *JsonValue { /* ... */ }
}
fn create(req: *HttpRequest) -> HttpResponse {
let user: StructLegal = StructLegal { nome: "alice", idade: some(30) };
return json_respond(user); // 200 + {"nome":"alice","idade":30}
}
IntoResponse
pub trait IntoResponse {
fn into_response(self: Self) -> HttpResponse;
}
Implemented for HttpResponse (identity) and for Json<T> when T: JsonBind (serialises T.to_json() into a 200). Use it for handler-tail ergonomics:
let r: HttpResponse = Json::wrap(user).into_response();
To bind an incoming JSON body, validate req.body_json() against T::from_json by hand (the typer doesn't yet accept a Json<T> fn-param destructure or a Json<T> handler return), then reply with json_respond.
URL-encoded forms — FormData
pub struct FormData { /* opaque (name, value) pairs */ }
An ordered list of (name, value) pairs for application/x-www-form-urlencoded bodies. Order is preserved and duplicate keys are kept.
FormData::new() -> *FormData— empty form.set(self: *FormData, name: string, value: string)— append a pair.len(self: *FormData) -> i32— pair count.name_at(self: *FormData, i: i32) -> string/value_at(self: *FormData, i: i32) -> string— walk by index.encode(self: *FormData) -> string— build the percent-encodedk=v&k=vwire string.
Free functions:
form_decode(s: string) -> !*FormData— parse ak=v&k=vbody; errs on bad percent-encoding.http_post_form(url: string, form: *FormData) -> !*HttpResponse— one-shot urlencoded POST.
import stdlib::http::form::*;
let f: *FormData = FormData::new();
f.set("user", "alice");
f.set("token", "abc xyz=99");
let body: string = f.encode(); // "user=alice&token=abc%20xyz%3D99"
let r: !*FormData = form_decode(body);
if r.ok {
for let i: i32 = 0; i < r.val.len(); i++ {
println!(r.val.name_at(i), "=", r.val.value_at(i));
}
}
let resp: !*HttpResponse = http_post_form("https://api.example.com/login", f);
if resp.ok { println!(resp.val.status); }
Multipart bodies — Multipart
pub struct Multipart { /* opaque boundary + parts */ }
Builds an outgoing multipart/form-data body (RFC 7578).
Multipart::new() -> *Multipart— random 24-char boundary.Multipart::with_boundary(boundary: string) -> *Multipart— fixed boundary (deterministic tests).add_field(self: *Multipart, name: string, value: string)— plain text field.add_file(self: *Multipart, name: string, filename: string, content_type: string, data: string)— file part (raw bytes).content_type(self: *Multipart) -> string—multipart/form-data; boundary=...for the Content-Type header.body(self: *Multipart) -> string— serialise all parts to the wire body.
import stdlib::http::multipart::*;
import stdlib::http::client::*;
let mp: *Multipart = Multipart::new();
mp.add_field("user", "alice");
mp.add_file("avatar", "pic.png", "image/png", bytes);
let c: *HttpClient = HttpClient::new();
let r: !*HttpResponse = c.post(url, mp.body(), mp.content_type());
if r.ok { println!(r.val.status); }
The receive side is MultipartForm (above), parsed via its FromRequest impl.
Client cookies — CookieJar
pub struct Cookie {
pub name: string,
pub value: string,
pub domain: string,
pub path: string,
pub secure: bool,
pub http_only: bool,
}
pub struct CookieJar { /* opaque cookie list */ }
CookieJar is the client-side store (RFC 6265-ish). Attach it to an HttpClient so Set-Cookie responses are absorbed and outbound requests carry Cookie: automatically.
Cookie:
Cookie::new(name: string, value: string) -> *Cookie— default scope (path="/", no domain restriction, not secure, not http-only). Adjust fields directly.
CookieJar:
CookieJar::new() -> *CookieJar— empty jar.ingest(self: *CookieJar, request_host: string, set_cookie_lines: *Vector<string>)— parse and store everySet-Cookieline, scoped torequest_host; same(name, domain, path)replaces.header_value(self: *CookieJar, host: string, path: string, is_secure: bool) -> string— build the outboundCookie:value;""when none applies.all(self: *CookieJar) -> *Vector<*Cookie>— snapshot of every cookie.clear(self: *CookieJar)— forget every cookie.
import stdlib::http::client::*;
import stdlib::http::cookies::*;
let jar: *CookieJar = CookieJar::new();
let c: *HttpClient = HttpClient::new();
c.jar = jar;
// Login: server's Set-Cookie is absorbed.
let _ = c.post_json("https://api.example.com/login", body);
// Subsequent requests carry Cookie: automatically.
let r: !*HttpResponse = c.get("https://api.example.com/me");
// Inspect the jar directly:
let h: string = jar.header_value("api.example.com", "/me", true);
// h might be "session=abc123"
jar.clear(); // logout
HTTP features — CORS, SSE, static, compression, proxy, JWT
A set of drop-in HTTP middlewares and helpers built on the Router / Chain pipeline and the HttpRequest / HttpResponse types. Each lives in its own stdlib::http::* submodule and is pub-imported individually:
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::cors::*; // CorsConfig / install_cors / cors_mw
import stdlib::http::sse::*; // SseEvent / SseChannel / sse_response ...
import stdlib::http::static::*; // StaticOpts / serve_dir
import stdlib::http::compress::*; // gzip_mw
import stdlib::http::proxy::*; // ReverseProxyConfig / reverse_proxy ...
import stdlib::http::jwt::*; // JwtClaims / jwt_verify
Middlewares share the signature fn(req: *HttpRequest, c: *Chain) -> HttpResponse and are registered with r.use_mw(...). Each calls chain_next(c) to invoke the rest of the pipeline, then post-processes the result. Helpers that touch the filesystem or network return !T and surface failures through .ok / .err.
CORS — CorsConfig
CorsConfig holds the values emitted as Access-Control-Allow-* response headers. install_cors stores them in a process-wide slot; cors_mw is the middleware that stamps them on every response (and short-circuits OPTIONS preflight requests with a 204).
pub struct CorsConfig {
pub origin: string, // Access-Control-Allow-Origin
pub methods: string, // Access-Control-Allow-Methods
pub headers: string, // Access-Control-Allow-Headers
pub credentials: bool, // emits Allow-Credentials: true when set
pub max_age: i32, // Access-Control-Max-Age seconds (0 = omit)
}
| Symbol | Signature | Description |
|---|---|---|
CorsConfig::permissive |
pub fn permissive() -> CorsConfig |
* origin, all common methods/headers, no credentials, no max-age. |
install_cors |
pub fn install_cors(cfg: CorsConfig) |
Store the config in the process-wide CORS slot read by cors_mw. |
cors_mw |
pub fn cors_mw(req: *HttpRequest, c: *Chain) -> HttpResponse |
Middleware: answers OPTIONS with 204, otherwise adds the CORS headers to chain_next(c). |
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::cors::*;
fn hello(_req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().text("hi");
}
fn main() -> i32 {
install_cors(CorsConfig::permissive());
let r: *Router = Router::new();
r.use_mw(cors_mw);
r.get("/hello", hello);
// GET /hello -> 200 + Access-Control-Allow-Origin: *
// OPTIONS /hello -> 204 + the CORS header block
return r.listen_workers(8080, 4).val;
}
For a locked-down policy build the config by hand:
install_cors(CorsConfig {
origin: "https://app.example.com",
methods: "GET, POST",
headers: "Content-Type, Authorization",
credentials: true,
max_age: 600,
});
// responses carry Allow-Credentials: true and Max-Age: 600
Server-Sent Events — SseEvent
SSE streams text/event-stream frames over chunked transfer. A handler returns an HttpResponse whose body is a *dyn ChunkSource; the server pulls one event at a time and frames it. SseEvent is the wire model.
pub struct SseEvent {
pub event: string, // event: name ("" = unnamed)
pub id: string, // id: value ("" = omit)
pub data: string, // data: payload (multi-line split per spec)
pub retry: i32, // retry: ms (0 = omit)
}
| Symbol | Signature | Description |
|---|---|---|
SseEvent::data |
pub fn data(payload: string) -> SseEvent |
Data-only event, no name/id/retry. |
SseEvent::named |
pub fn named(name: string, payload: string) -> SseEvent |
Named event (evtSource.addEventListener(name, ...)). |
sse_format |
pub fn sse_format(e: SseEvent) -> string |
Serialize an event to wire bytes; an empty event is a single blank line. |
sse_comment |
pub fn sse_comment(text: string) -> string |
A : text\n\n comment line (heartbeat). |
sse_keepalive |
pub fn sse_keepalive() -> string |
Minimal :\n\n heartbeat frame. |
sse_last_event_id |
pub fn sse_last_event_id(req: *HttpRequest) -> string |
The client's Last-Event-ID header ("" when absent). |
sse_response |
pub fn sse_response(source: *dyn ChunkSource) -> HttpResponse |
Wrap a ChunkSource with the SSE content-type + no-buffer headers. |
SSE_END |
pub const SSE_END: string |
Sentinel event name that closes an SseChannel stream ("__close__"). |
import stdlib::http::*;
import stdlib::http::sse::*;
fn main() -> i32 {
let e: SseEvent = SseEvent::named("tick", "1");
println!(sse_format(e)); // "event: tick\ndata: 1\n\n"
println!(sse_format(SseEvent::data("hi"))); // "data: hi\n\n"
println!(sse_keepalive()); // ":\n\n"
return 0;
}
A producer-driven stream implements ChunkSource::next_chunk and returns some(sse_format(...)) per event, none() at end:
import stdlib::http::*;
import stdlib::http::sse::*;
pub struct Pulse { pub idx: i32 }
impl ChunkSource for Pulse {
fn next_chunk(self: *Pulse) -> ?string {
if self.idx >= 5 { return none(); }
self.idx = self.idx + 1;
return some(sse_format(SseEvent::data(self.idx.to_string())));
}
}
fn ticks(_req: *HttpRequest) -> HttpResponse {
let p: *Pulse = malloc(sizeof(Pulse)) as *Pulse;
p.idx = 0;
return sse_response(p as *dyn ChunkSource); // streams data: 1..5
}
SseChannel fans a broadcast chan<SseEvent> out to a client. Each value sent into the chan becomes a frame; sending an event whose .event equals SSE_END ends the stream.
pub struct SseChannel { pub ch: *chan<SseEvent> }
| Symbol | Signature | Description |
|---|---|---|
SseChannel::new |
pub fn new(ch: *chan<SseEvent>) -> *SseChannel |
Wrap a chan as a ChunkSource. |
import stdlib::http::*;
import stdlib::http::sse::*;
let events: chan<SseEvent> = make_chan(64);
fn stream(_req: *HttpRequest) -> HttpResponse {
let src: *SseChannel = SseChannel::new(&events);
return sse_response(src as *dyn ChunkSource);
}
// elsewhere: events.send(SseEvent::named("ping", "1"));
// to close: events.send(SseEvent { event: SSE_END, id: "", data: "", retry: 0 });
Static files — StaticOpts
serve_dir mounts a directory at a URL prefix and serves files through HttpResponse::file (zero-copy sendfile). It adds path-traversal protection (rejects .., leading /, NUL bytes), a weak ETag from file size with If-None-Match → 304 support, an optional Cache-Control, and index.html for directory paths.
pub struct StaticOpts {
pub root: string, // directory on disk
pub url_prefix: string, // URL segment files mount under
pub cache_control: string, // Cache-Control header ("" = omit)
}
| Symbol | Signature | Description |
|---|---|---|
serve_dir |
pub fn serve_dir(r: *Router, opts: StaticOpts) |
Register GET <url_prefix>/*filepath serving <root>/.... |
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::static::*;
fn main() -> i32 {
let r: *Router = Router::new();
serve_dir(r, StaticOpts {
root: "./public",
url_prefix: "/static",
cache_control: "public, max-age=3600",
});
// GET /static/css/site.css -> ./public/css/site.css (+ ETag, Cache-Control)
// GET /static/ -> ./public/index.html
// GET /static/../etc -> 403 forbidden
return r.listen_workers(8080, 4).val;
}
Compression — gzip_mw
gzip_mw compresses the response body with gzip (zlib, linked -lz) after running the rest of the chain. It only kicks in when the request advertised Accept-Encoding: gzip, the status is 2xx, no Content-Encoding is already set, the body is ≥ 1024 bytes, and the Content-Type is not an already-compressed media type (image/video/audio, zip, gzip, octet-stream). When it compresses it replaces the body and sets Content-Encoding: gzip.
| Symbol | Signature | Description |
|---|---|---|
gzip_mw |
pub fn gzip_mw(req: *HttpRequest, c: *Chain) -> HttpResponse |
Gzip the downstream response when the conditions above hold. |
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::compress::*;
fn page(_req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok()
.set("Content-Type", "text/html; charset=utf-8")
.text(big_html()); // > 1024 bytes
}
fn main() -> i32 {
let r: *Router = Router::new();
r.use_mw(gzip_mw);
r.get("/", page);
// request with Accept-Encoding: gzip -> body gzipped, Content-Encoding: gzip
// tiny / non-gzip-accepting requests -> body passes through untouched
return r.listen_workers(8080, 4).val;
}
Reverse proxy — ReverseProxyConfig
reverse_proxy forwards an incoming request to an upstream origin and lowers the upstream response back into the server's HttpResponse. It strips hop-by-hop headers, stamps X-Forwarded-*, optionally strips a path prefix, and turns network/parse errors into 502 Bad Gateway.
pub struct ReverseProxyConfig {
pub upstream: string, // origin, e.g. "http://127.0.0.1:9000"
pub strip_prefix: string, // remove from path before forwarding
pub preserve_host: bool, // keep client Host header upstream
pub rewrite_location: bool, // rewrite upstream Location to public_base
pub public_base: string, // base used by rewrite_location
pub timeout_ms: i32, // upstream request timeout
pub tls_insecure: bool, // skip upstream TLS verification
}
| Symbol | Signature | Description |
|---|---|---|
ReverseProxyConfig::to |
pub fn to(upstream: string) -> ReverseProxyConfig |
Config with conservative defaults (30s timeout, no prefix strip, verify TLS). |
reverse_proxy |
pub fn reverse_proxy(cfg: *ReverseProxyConfig, req: *HttpRequest) -> HttpResponse |
Forward req upstream; 502 on failure. |
proxy_build_upstream_request |
pub fn proxy_build_upstream_request(cfg: *ReverseProxyConfig, req: *HttpRequest) -> *HttpClientRequest |
Build (but don't send) the upstream client request. |
proxy_target_url |
pub fn proxy_target_url(cfg: *ReverseProxyConfig, path: string) -> string |
Compute the upstream URL for a request path (applies strip_prefix). |
proxy_filter_request_headers |
pub fn proxy_filter_request_headers(block: string, preserve_host: bool) -> string |
Drop hop-by-hop (and Host unless preserved) from a request header block. |
proxy_filter_response_headers |
pub fn proxy_filter_response_headers(block: string) -> string |
Drop hop-by-hop / framing headers from an upstream response block. |
proxy_rewrite_location |
pub fn proxy_rewrite_location(loc: string, upstream: string, public_base: string) -> string |
Rewrite an absolute upstream Location to public_base; relative ones pass through. |
import stdlib::http::*;
import stdlib::http::proxy::*;
fn main() -> i32 {
let cfg: ReverseProxyConfig = ReverseProxyConfig::to("http://127.0.0.1:9000");
http_listen(8080, fn(req) { return reverse_proxy(&cfg, req); });
// GET /users -> http://127.0.0.1:9000/users, response relayed back
return 0;
}
Mount an upstream under a local prefix and rewrite redirects to the public host:
import stdlib::http::*;
import stdlib::http::proxy::*;
fn main() -> i32 {
let cfg: ReverseProxyConfig = ReverseProxyConfig {
upstream: "http://127.0.0.1:9000",
strip_prefix: "/api",
preserve_host: false,
rewrite_location: true,
public_base: "https://edge.example.com",
timeout_ms: 10000,
tls_insecure: false,
};
// /api/users?x=1 -> http://127.0.0.1:9000/users?x=1
println!(proxy_target_url(&cfg, "/api/users?x=1"));
// "http://127.0.0.1:9000/users?x=1"
http_listen(8080, fn(req) { return reverse_proxy(&cfg, req); });
return 0;
}
JWT verification — JwtClaims
jwt_verify checks a compact HS256 JWT against a shared secret: it validates the three-part structure, recomputes the HMAC-SHA-256 signature over header.payload, requires a string sub claim, and rejects expired tokens (exp in the past). On success it returns the decoded claims; errors carry an HTTP-status prefix ("400:...", "401:...").
pub struct JwtClaims {
pub sub: string, // subject claim (required)
pub exp: i32, // expiry epoch seconds (0 = no exp claim)
pub iat: i32, // issued-at epoch seconds (0 = absent)
pub raw: *JsonValue, // full decoded payload object
}
| Symbol | Signature | Description |
|---|---|---|
jwt_verify |
pub fn jwt_verify(token: string, key: string) -> !JwtClaims |
Verify an HS256 token; err on malformed/bad-sig/expired. |
import stdlib::http::*;
import stdlib::http::jwt::*;
fn main() -> i32 {
let token: string = req_token(); // "<header>.<payload>.<sig>"
let r: !JwtClaims = jwt_verify(token, "my-shared-secret");
if !r.ok {
println!(r.err); // "401:bad signature" / "401:expired"
return 1;
}
let claims: JwtClaims = r.val;
println!(claims.sub); // e.g. "user-42"
println!(claims.exp.to_string()); // e.g. "1900000000"
return 0;
}
Used inside a handler, the status-prefixed error maps cleanly onto a response:
import stdlib::http::*;
import stdlib::http::jwt::*;
fn protected(req: *HttpRequest) -> HttpResponse {
let auth: string = req.header("Authorization"); // "Bearer <token>"
let token: string = auth.substring(7, auth.len());
let r: !JwtClaims = jwt_verify(token, "my-shared-secret");
if !r.ok {
return HttpResponse::with_status(401).text("unauthorized");
}
return HttpResponse::ok().text("welcome ".concat(r.val.sub));
}
HTTP/2 & HTTP/3 — H2Conn
Two newer transports sit on top of the TLS and UDP layers from earlier in this chapter. HTTP/2 (stdlib::http::h2) is pure Glide: framing, a connection state machine, and HPACK header compression, with both a client (H2Conn) and a server loop (h2_serve / h2_listen). HTTP/3 (stdlib::http::h3) wraps QUIC via libngtcp2 + libnghttp3 and exposes a small set of free functions that mirror the HTTP/1.1 client shape.
Both negotiate the protocol through TLS ALPN. The client opts into "h2,http/1.1" (or "h3") so a server that doesn't speak the new version falls back transparently. v1 of each transport is GET-and-simple-POST oriented: no PUSH, no priority obey, no trailers.
Platform caveat for HTTP/3
HTTP/3 links -lngtcp2 -lngtcp2_crypto_ossl -lnghttp3 -lssl -lcrypto and needs OpenSSL 3.5+'s QUIC API plus the static ngtcp2_crypto_ossl shim. That combination is present in the windows-gnu sysroot shipped with the release, but not in the musl (Linux) or macOS sysroots today. In practice HTTP/3 is Windows-only in the shipped sysroots — on Linux/macOS the linker falls back to system libs, which only resolve if you installed an OpenSSL 3.5+ / ngtcp2 toolchain yourself. HTTP/2 has no such dependency; it works on every target.
FrameType and FrameFlag — HTTP/2 opcodes
HTTP/2 frames carry a 24-bit length, an 8-bit type, 8-bit flags, and a 31-bit stream id (RFC 7540 §4.1). FrameType and FrameFlag are empty marker structs whose static methods return the spec constants — use them instead of literal integers.
FrameType methods (each returns i32):
FrameType::data() // 0
FrameType::headers() // 1
FrameType::priority() // 2
FrameType::rst_stream() // 3
FrameType::settings() // 4
FrameType::push_promise() // 5
FrameType::ping() // 6
FrameType::goaway() // 7
FrameType::window_update() // 8
FrameType::continuation() // 9
FrameFlag methods (each returns i32, combine with |):
FrameFlag::end_stream() // 1
FrameFlag::end_headers() // 4
FrameFlag::padded() // 8
FrameFlag::priority() // 32
FrameFlag::ack() // 1 (shares bit 1 with end_stream; never on the same frame)
FrameHeader — a decoded frame header
pub struct FrameHeader {
pub length: i32, // 24-bit payload length
pub frame_type: i32, // 8-bit type
pub flags: i32, // 8-bit flags
pub stream_id: i32, // 31-bit stream id (top bit reserved)
}
Frame encode / decode functions
write_frame(buf: *ByteBuffer, frame_type: i32, flags: i32, stream_id: i32, payload: string)— append a 9-byte header + the string payload. Truncates at the first NUL (Glidestringis a cstring), so use it only for text payloads.write_frame_bytes(buf: *ByteBuffer, frame_type: i32, flags: i32, stream_id: i32, payload: *ByteBuffer)— same, but the payload comes from aByteBufferwith explicit length. Binary-safe; use this for HPACK header blocks.read_frame_header(raw: *ByteBuffer, pos: i32) -> !*FrameHeader— parse a 9-byte header atpos.h2_preface() -> string— the 24-byte connection preface"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".settings_payload_empty() -> string— an empty SETTINGS payload (used to ACK the peer's settings).settings_pair_to(buf: *ByteBuffer, key: i32, value: i32)— append one 6-byte SETTINGS entry.window_update_to(buf: *ByteBuffer, increment: i32)— append a 4-byte WINDOW_UPDATE payload.
import stdlib::bytes::*;
import stdlib::http::h2::*;
let buf: *ByteBuffer = ByteBuffer::new();
defer buf.free();
write_frame(buf, FrameType::ping(), 0, 0, "ABCDEFGH"); // 8-byte PING
let hr: !*FrameHeader = read_frame_header(buf, 0);
if hr.ok {
let h: *FrameHeader = hr.val;
println!(h.frame_type, h.length, h.stream_id); // => 6 8 0
}
HpackEncoder / HpackDecoder — HPACK header compression
HPACK (RFC 7541) compresses HTTP/2 header fields against a 61-entry static table plus a per-direction dynamic table. The encoder never emits Huffman literals (spec-legal, H=0 always); the decoder fully supports Huffman because real servers emit it. Reuse one encoder and one decoder for the lifetime of a connection so the dynamic table builds up.
pub struct HpackEncoder { pub max_table_size: i32, /* internal tables */ }
pub struct HpackDecoder { pub max_table_size: i32, /* internal tables */ }
/// One decoded header field, in wire order.
pub struct HpackHeader {
pub name: string,
pub value: string,
}
HpackEncoder:
HpackEncoder::new() -> *HpackEncoder— 4 KiB dynamic table cap.encode_header(self, buf: *ByteBuffer, name: string, value: string)— append one header tobuf.
HpackDecoder:
HpackDecoder::new() -> *HpackDecoder— 4 KiB dynamic table cap.decode_block(self, raw: string, len: i32) -> !*Vector<*HpackHeader>— decode a full header block.lenis explicit because HPACK blocks frequently contain NUL bytes.
import stdlib::bytes::*;
import stdlib::net::ip::*; // _bytes_to_string
import stdlib::http::hpack::*;
let enc: *HpackEncoder = HpackEncoder::new();
let block: *ByteBuffer = ByteBuffer::new();
defer block.free();
enc.encode_header(block, ":method", "GET");
enc.encode_header(block, ":path", "/");
// Round-trip through the decoder.
let dec: *HpackDecoder = HpackDecoder::new();
let raw: string = _bytes_to_string(block.data, block.len);
let r: !*Vector<*HpackHeader> = dec.decode_block(raw, block.len);
if r.ok {
for let i: i32 = 0; i < r.val.len(); i++ {
let h: *HpackHeader = r.val.get(i);
println!(h.name, "=", h.value); // => :method = GET / :path = /
}
}
H2Conn — HTTP/2 client connection
H2Conn wraps an already-handshaked TlsStream that negotiated ALPN h2. It sends the connection preface and an initial SETTINGS + WINDOW_UPDATE on construction, then request opens a stream, sends headers (+ optional body), and reassembles the response.
pub struct H2Conn {
pub next_stream_id: i32, // next odd client stream id
/* tls, HPACK codecs, rx ring are internal */
}
Methods:
H2Conn::over(tls: *TlsStream) -> !*H2Conn— adopt a handshakedh2TLS stream; sends preface + SETTINGS (ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=1 MiB) + a connection WINDOW_UPDATE.request(self, method: string, path: string, authority: string, scheme: string, extra_headers: *Vector<*HpackHeader>, body: string) -> !*H2Response— send one request, read the full response.close(self)— send GOAWAY and close the underlying TLS stream.
The response:
pub struct H2Response {
pub status: i32,
pub headers: *Vector<*HpackHeader>,
pub body: string,
}
A complete client request. Note the ALPN negotiation: only proceed with H2Conn::over if the server actually selected h2.
import stdlib::net::tls::*;
import stdlib::http::hpack::*;
import stdlib::http::h2::*;
fn fetch_h2() -> i32 {
let cfg: *TlsConfig = TlsConfig::client();
cfg.alpn = "h2,http/1.1";
let ts: !*TlsStream = TlsStream::connect("nghttp2.org", 443, cfg);
if !ts.ok { return 1; }
if !ts.val.alpn().eq("h2") {
// Server fell back to HTTP/1.1 — drive it over the 1.1 client instead.
ts.val.close();
return 2;
}
let h2r: !*H2Conn = H2Conn::over(ts.val);
if !h2r.ok { return 3; }
let h2: *H2Conn = h2r.val;
defer h2.close();
let extras: *Vector<*HpackHeader> = Vector::new(); // no extra headers
let r: !*H2Response = h2.request("GET", "/", "nghttp2.org", "https", extras, "");
if !r.ok { return 4; }
println!("status:", r.val.status); // => status: 200
println!("bytes:", r.val.body.len());
return 0;
}
To add request headers, push HpackHeader entries onto the extra_headers vector. The connection stays open after request returns, so a second call reuses the same HPACK dynamic table:
let extras: *Vector<*HpackHeader> = Vector::new();
let h: *HpackHeader = malloc(sizeof(HpackHeader)) as *HpackHeader;
h.name = "accept";
h.value = "application/json";
extras.push(h);
let r1: !*H2Response = h2.request("GET", "/v1/a", "api.example.com", "https", extras, "");
let r2: !*H2Response = h2.request("GET", "/v1/b", "api.example.com", "https", extras, "");
Most users reach HTTP/2 through the high-level client instead: set c.http2 = true on an HttpClient and it negotiates h2 over ALPN for https:// URLs, falling back to HTTP/1.1 when the server declines.
import stdlib::http::*;
let c: *HttpClient = HttpClient::new();
c.http2 = true;
let r: !*HttpResponse = c.get("https://nghttp2.org/");
if r.ok { println!(r.val.status); } // => 200
H2ServerStream + h2_serve — HTTP/2 server
The server side drives one client connection's full lifetime: validate the preface, exchange SETTINGS, then dispatch each fully-received request to a handler. The handler runs synchronously inside the read loop; concurrent streams on one connection are serialized at the handler step.
/// Per-stream state the server tracks while a request is arriving.
pub struct H2ServerStream {
pub id: i32,
pub header_block: *ByteBuffer,
pub headers_done: bool,
pub body: *ByteBuffer,
pub closed_remote: bool,
}
h2_serve(s: *TlsStream, handler: fn(*HttpRequest) -> HttpResponse)— run the HTTP/2 server loop on an already-handshakedh2TLS stream. Closes the stream when the connection ends; the caller does not calls.close().
The handler is the same fn(*HttpRequest) -> HttpResponse shape used everywhere else in this chapter, so a handler written for HTTP/1.1 works unchanged:
import stdlib::net::tls::*;
import stdlib::http::*;
import stdlib::http::h2::*;
fn handle(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("served over h2");
}
// Inside your own TLS accept loop, once ALPN negotiated `h2`:
fn serve_conn(client: *TlsStream) {
h2_serve(client, handle); // takes ownership; closes when done
}
HTTP/3 over QUIC
HTTP/3 is exposed as free functions plus a session-cache type. Remember the Windows-only sysroot caveat above. URLs must be https:// (or h3://); the default port is 443.
Client requests
http3_get(url: string, tls_insecure: bool) -> !*HttpResponse— single-shot GET.http3_request(method: string, url: string, extra_headers: string, body: string, tls_insecure: bool) -> !*HttpResponse— generic single-shot request.extra_headersis a CRLF-joinedName: Valueblock.
tls_insecure = true disables certificate verification (local self-signed dev only). Both return the familiar HttpResponse.
import stdlib::http::*;
import stdlib::http::h3::*;
fn get_over_quic() -> i32 {
let r: !*HttpResponse = http3_get("https://cloudflare-quic.com/", false);
if !r.ok { println!("h3 failed:", r.err); return 1; }
println!("status:", r.val.status); // => status: 200
println!(r.val.body);
return 0;
}
let r: !*HttpResponse = http3_request(
"POST", "https://api.example.com/x",
"Content-Type: application/json\r\n",
"{\"hi\":1}", false);
if r.ok { println!(r.val.status); }
ParsedH3Url
The URL parser used internally is also public:
pub struct ParsedH3Url {
pub host: string,
pub port: i32,
pub path: string,
}
H3SessionCache — 0-RTT session resumption
Threading an H3SessionCache across repeated http3_request_cached calls lets the second and later connections to the same (host, port) resume the TLS session and ship the request as 0-RTT early data (saves ~1 RTT). In-memory by default; save/load persist it to a binary file across restarts.
pub struct H3SessionCache { /* opaque handle */ }
Methods:
H3SessionCache::new() -> *H3SessionCachefree(self)— release the cache.attempts(self) -> i32— connects that found and installed a cached session (tried 0-RTT).accepted(self) -> i32— connects whose 0-RTT data the server accepted.attempts - acceptedis the rejection count.save(self, path: string) -> bool— serialise to a binary file.load(self, path: string) -> i32— read entries back (drops expired; returns count loaded, 0 on a missing/corrupt file).
And the cached request entry point:
http3_request_cached(method: string, url: string, extra_headers: string, body: string, tls_insecure: bool, cache: *H3SessionCache) -> !*HttpResponse— same shape ashttp3_requestbut threads the cache. Thread-safe (internal mutex).
import stdlib::http::*;
import stdlib::http::h3::*;
fn resume_demo(url: string) -> i32 {
let c: *H3SessionCache = H3SessionCache::new();
defer c.free();
c.load("h3-sessions.bin"); // 0 first run; safe before save ever ran
let r1: !*HttpResponse = http3_request_cached("GET", url, "", "", false, c);
if !r1.ok { return 1; } // full handshake; ticket cached
let r2: !*HttpResponse = http3_request_cached("GET", url, "", "", false, c);
if !r2.ok { return 2; } // resumes the prior session, may 0-RTT
println!("0-RTT attempts:", c.attempts(), "accepted:", c.accepted());
c.save("h3-sessions.bin");
return 0;
}
HTTP/3 server
h3_make_self_signed_cert(cert_path: string, key_path: string, cn: string, days: i32) -> !i32— write an ECDSA-P256 self-signed PEM cert + key (SAN coversDNS:localhostandIP:127.0.0.1), good fordaysdays. Useful for local dev / loopback tests.http3_listen(port: i32, cert_path: string, key_path: string, handler: fn(*HttpRequest) -> HttpResponse) -> !i32— run an HTTP/3 server on the UDPport. A single C reader thread demuxes inbound packets across up to 64 connections; each accepted connection's request is dispatched tohandleron its own pthread.
cert_path / key_path are PEM files (same layout as https_listen). The handler signature is identical to the HTTP/1.1 and HTTP/2 servers.
import stdlib::http::*;
import stdlib::http::h3::*;
fn root(req: *HttpRequest) -> HttpResponse {
return HttpResponse::ok().body("hello over QUIC");
}
fn main() -> i32 {
let cert: !i32 = h3_make_self_signed_cert("cert.pem", "key.pem", "localhost", 365);
if !cert.ok { println!(cert.err); return 1; }
let r: !i32 = http3_listen(4443, "cert.pem", "key.pem", root);
if !r.ok { println!(r.err); return 1; } // err on bind / bad cert
return 0;
}
v1 server limits: one request stream per connection, no HTTP/3 PUSH, no path-MTU discovery (assumes a 1200-byte safe default). 0-RTT early data is accepted on the server but only for idempotent methods — non-GET/HEAD requests arriving as 0-RTT are rejected with 425 Too Early so the client retries over 1-RTT.
URL encoding — url_encode
Percent-encoding and the parsed-URL structs that the HTTP and WebSocket clients build internally. The codec lives in stdlib::url; the parsed structs ship with their respective clients (http::ParsedUrl, net::WsParsedUrl).
import stdlib::url::*;
url_encode / url_decode
pub fn url_encode(s: string) -> string
pub fn url_decode(s: string) -> !string
url_encode percent-encodes s per RFC 3986 §2.3. The unreserved set — ASCII letters, digits, and - _ . ~ — passes through untouched; every other byte becomes %XX with upper-case hex. Multibyte UTF-8 is encoded byte by byte.
url_encode("hello world"); // => "hello%20world"
url_encode("a&b=c"); // => "a%26b%3Dc"
url_encode("test_~-."); // => "test_~-." (all unreserved)
url_encode("héllo"); // => "h%C3%A9llo" (UTF-8 bytes)
url_decode is the inverse. It walks the string, expanding each %XX back into a byte and copying everything else verbatim. Note that + is not treated as a space — it is preserved literally, matching what url_encode produces (a space becomes %20, never +). A % at the end of the string or one followed by a non-hex digit is a hard error, so the function returns !string: read .ok, then .val or .err.
let r: !string = url_decode("hello%20world");
if r.ok { println!(r.val); } // "hello world"
url_decode("%c3%a9").val; // => "é" (lower-case hex accepted)
url_decode("a+b").val; // => "a+b" (plus kept literal)
let bad: !string = url_decode("%ZZ");
if !bad.ok { println!(bad.err); } // "url_decode: malformed percent escape"
let short: !string = url_decode("%2");
if !short.ok { println!(short.err); } // malformed (truncated escape)
Encode/decode round-trip is lossless over arbitrary input:
let src: string = "azAZ09-_.~/?:#[]@!$&'()*+,;= hé";
let enc: string = url_encode(src);
let dec: !string = url_decode(enc);
if dec.ok {
println!(dec.val.eq(src)); // => true
}
Building and parsing query strings
There is no dedicated query-map builder in stdlib::url — a query string is just an ordinary URL component, so you compose one by encoding each key and value with url_encode and joining them yourself:
let q: string = "q=".concat(url_encode("hello world"))
.concat("&lang=").concat(url_encode("en"));
// q => "q=hello%20world&lang=en"
On the server side, the query string is parsed for you: an http::HttpRequest exposes .query(name) (covered in the HTTP server section), so application code rarely decodes query parameters by hand.
ParsedUrl — HTTP URLs
The HTTP client parses an http:// or https:// URL into a ParsedUrl. The struct is public so you can read the components a request was routed to; the parser itself is internal (constructed by HttpClient), so you normally obtain one indirectly rather than building it.
pub struct ParsedUrl {
pub scheme: string, // "http" or "https"
pub host: string, // bare host (IPv6 literals unwrapped from [..])
pub port: i32, // explicit port, else 80 (http) / 443 (https)
pub path: string, // path + query, never empty ("/" when absent)
}
port defaults from the scheme (80 for http, 443 for https) and is overridden by an explicit :port. IPv6 hosts are accepted in bracket form (https://[::1]:8443/) and the brackets are stripped from host.
host_header
pub fn host_header(self: *ParsedUrl) -> string
Formats the value for an HTTP Host: header. The port is omitted when it equals the scheme default, and included otherwise.
// https://example.com/ -> host_header() == "example.com"
// https://example.com:8443/x -> host_header() == "example.com:8443"
WsParsedUrl — WebSocket URLs
The WebSocket client uses a parallel struct for ws:// / wss:// URLs. It carries a boolean is_wss flag instead of a scheme string, but is otherwise the same shape as ParsedUrl.
pub struct WsParsedUrl {
pub is_wss: bool, // false = ws, true = wss
pub host: string,
pub port: i32, // explicit port, else 80 (ws) / 443 (wss)
pub path: string, // never empty ("/" when absent)
}
host_header
pub fn host_header(self: *WsParsedUrl) -> string
Same contract as ParsedUrl::host_header: drops the port when it matches the scheme default (80 for ws, 443 for wss), keeps it otherwise.
// wss://example.com/ -> host_header() == "example.com"
// wss://example.com:8443/ -> host_header() == "example.com:8443"
ParsedUrl and WsParsedUrl are deliberately the same four fields (host / port / path plus a scheme discriminator) so the connection logic in both clients is identical: resolve host, dial port, send path, and emit host_header() as the Host: header. The only difference is which default ports and scheme names apply.