Chapter 28 89 min read

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 FormDataapplication/x-www-form-urlencoded body builder + parser.
stdlib::http::multipart Multipartmultipart/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() -> *IpAddr127.0.0.1.
  • IpAddr::loopback_v6() -> *IpAddr::1.
  • IpAddr::unspec_v4() -> *IpAddr0.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) -> booltrue if this is a v4 address.
  • is_v6(self: *IpAddr) -> booltrue if this is a v6 address.
  • is_loopback(self: *IpAddr) -> booltrue for 127.0.0.0/8 (v4) or ::1 (v6).
  • is_unspecified(self: *IpAddr) -> booltrue for the all-zeros address.
  • is_private(self: *IpAddr) -> booltrue for RFC 1918 (10/8, 172.16/12, 192.168/16) on v4 or fc00::/7 unique-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). Returns err on 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 as host: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 wrapped IpAddr and the SocketAddr struct 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 to max bytes into
  • the caller's buffer; returns the count (0 = peer closed).

  • stream.write_bytes(buf: *u8, n: i32) -> !i32 — write n bytes from buf.
  • stream.close(self) — close the socket and free the struct. After close()
  • 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 — resolve host via 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-connected TcpStream (STARTTLS-style upgrade). On success the returned stream owns the fd; do not call tcp.close() afterwards — the TcpStream wrapper is freed for you.

I/O

  • stream.write(data: string) -> !i32 — write data; 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 to max bytes; 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 into buf (Stream trait).
  • stream.write_bytes(buf: *u8, n: i32) -> !i32 — binary-safe write (Stream trait); unlike write, does not truncate at NUL.

Connection info

  • stream.alpn() -> string — negotiated ALPN protocol, or "" if none.
  • stream.version() -> i322 for TLS 1.2, 3 for TLS 1.3, 0 otherwise.
  • 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 — bind 0.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 as err.
  • 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 from accept_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. family is AF_V4/AF_V6, ty is SOCK_STREAM/SOCK_DGRAM/SOCK_RAW, proto is 0 for 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) -> !i32
  • accept(self: *Socket) -> !*Socket — accept one connection, returning a fresh owning Socket for 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/opt come from gnet_const.
  • set_reuse_addr(self: *Socket, on: bool) -> !i32SO_REUSEADDR.
  • shutdown(self: *Socket, how: i32) -> !i32how is SHUT_RD/SHUT_WR/SHUT_RDWR.
  • send(self: *Socket, buf: *void, n: i32) -> !i32 — send n bytes; returns bytes sent.
  • recv(self: *Socket, buf: *void, max: i32) -> !i32 — receive up to max bytes; returns bytes read (0 at 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 to max bytes 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) -> !i32SO_REUSEADDR, rebind a recently-closed port immediately.
  • sock_set_reuse_port(fd: i64, on: bool) -> !i32SO_REUSEPORT, load-balance accepts across workers (Linux).
  • sock_set_nodelay(fd: i64, on: bool) -> !i32TCP_NODELAY, disable Nagle so small writes flush at once.
  • sock_set_broadcast(fd: i64, on: bool) -> !i32SO_BROADCAST, allow sending to a broadcast address (UDP).
  • sock_set_ttl(fd: i64, ttl: i32) -> !i32IP_TTL, outgoing unicast hop limit.
  • sock_set_recv_buf(fd: i64, bytes: i32) -> !i32SO_RCVBUF size hint.
  • sock_set_send_buf(fd: i64, bytes: i32) -> !i32SO_SNDBUF size hint.
  • sock_set_timeout_ms(fd: i64, ms: i32) -> !i32 — read + write timeout in ms (0 disables); backed by SO_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 for proto (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 — send n bytes to dst; 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 (0 disables).
  • 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, returns 0 (the validity check).
  • icmp_build_echo(buf: *u8, id: i32, seq: i32, payload_len: i32) -> i32 — assemble an echo-request into buf (needs 8 + payload_len bytes); returns the total length.
  • ping(host: string, timeout_ms: i32) -> !i64 — ping host once; returns the round-trip time in milliseconds.
  • traceroute(host: string, max_hops: i32, timeout_ms: i32) -> !*Vector<string> — probe TTL 1..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 an
  • envelope with empty headers and body.

  • pub fn set(self, name: string, value: string) — append one header line.
  • Multiple calls add multiple lines; there is no dedup, so last-wins is the caller's responsibility.

  • pub fn body_str(self, s: string) — set the body and the matching
  • 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 the User-Agent request header.
  • max_redirects — maximum redirect hops do follows before returning
  • err("http_client: too many redirects"). do_once ignores this.

  • jar — assign a CookieJar::new() to enable automatic Set-Cookie
  • ingestion plus Cookie: header attachment across requests. Stays null (no cookie handling) by default.

  • tls_insecure — when true, skip TLS certificate validation. Local dev
  • only; production code MUST leave this off.

  • timeout_ms — per-request total timeout in milliseconds (connect + send +
  • receive), applied via socket send/receive timeouts. 0 disables it.

  • http2 — try HTTP/2 over TLS via ALPN, falling back to HTTP/1.1 when the
  • 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 and
  • 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.

  • pub fn do_once(self, req: *HttpClientRequest) -> !*HttpResponse — send
  • exactly one request, never following redirects. Use for proxies and crawlers that must preserve 30x responses verbatim.

  • pub fn get(self, url: string) -> !*HttpResponse — one-shot GET via do.
  • pub fn post(self, url: string, body: string, content_type: string) -> !*HttpResponse
  • — POST with an explicit Content-Type.

  • pub fn post_json(self, url: string, body: string) -> !*HttpResponse
  • POST with Content-Type: application/json. The caller serialises the JSON.

  • pub fn put(self, url: string, body: string, content_type: string) -> !*HttpResponse
  • — PUT with an explicit Content-Type.

  • pub fn delete(self, url: string) -> !*HttpResponse — DELETE.

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 the Host: 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-insensitive
  • header lookup, "" when missing. First call parses headers_block into a cache; later calls are O(1). Repeated headers keep the first value.

  • param(self: *HttpRequest, name: string) -> string — router path
  • parameter, "" when unknown or dispatched without a router.

  • query(self: *HttpRequest, name: string) -> string — decoded
  • query-string value, "" when missing. Parsed + cached on first access.

  • path_only(self: *HttpRequest) -> stringpath with the ?...
  • query segment stripped.

  • is_method(self: *HttpRequest, m: string) -> bool — case-insensitive
  • method check.

  • body_json(self: *HttpRequest) -> !*JsonValue — parse the body as JSON;
  • 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, no
  • body, no headers.

  • HttpResponse::ok() -> HttpResponse — 200, no body.
  • HttpResponse::not_found() -> HttpResponse — 404.
  • HttpResponse::redirect(url: string) -> HttpResponse — 302 with
  • Location: url.

  • HttpResponse::file(path: string, mime: string) -> HttpResponse — serve
  • 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 matching Content-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) — serialise v.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 existing
  • same-name line (case-insensitive). CR/LF in name/value is stripped.

  • add(self, name, value: string) — append a header line, preserving
  • same-name lines (for Set-Cookie, Vary, Link).

  • cookie(self, name, value: string) — append a Set-Cookie: name=value
  • header (routes through add).

Header getters (take *HttpResponse):

  • header(self: *HttpResponse, name: string) -> string
  • case-insensitive lookup, "" when missing.

  • headers(self: *HttpResponse, name: string) -> *Vector<string> — every
  • 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) -> !i32n workers,
  • each with its own SO_REUSEPORT listener.

Lower-level helpers

  • parse_http_request(raw: string) -> !HttpRequest — parse a complete
  • HTTP/1.1 request from raw bytes; err on a malformed or partial request.

  • format_http_response(r: *HttpResponse) -> string — render a response
  • 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:

  • /usersliteral, must match byte-for-byte.
  • /users/:idparam, captures one segment, read via req.param("id").
  • /static/*restwildcard, captures the rest of the path (zero or more
  • segments) into req.param("rest"). A wildcard may only appear last.

Method dispatch is exact (GETPOST) 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) — register handler for
  • method + pattern. The method is stored upper-case; pass ANY_METHOD (or "*") for every verb.

  • get / post / put / delete / patch / head / options (self, pattern, handler)
  • — verb shortcuts for route(...).

  • any(self, pattern, handler) — match every HTTP method on pattern.
  • route_with(self, method, pattern, mws, handler) — like route but with a
  • per-route middleware list (router-level mw runs first, then mws, then the handler).

  • not_found_with(self, handler) — override the default 404 handler.
  • free(self) — release the route table and the router struct; pair with
  • 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 the
  • reactor + per-conn coroutines; Windows / macOS / BSD run serially until their reactors land.

  • listen_workers(self, port, n) -> !i32 — routes through the C
  • state-machine HTTP server for max throughput. Handlers MUST be non-blocking (no chan.send/recv, no sleep_ms, no parking I/O).

  • listen_workers_blocking(self, port, n) -> !i32 — coroutine-per-conn
  • 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; runs
  • before the handler in registration order. Returns self for chaining.

  • scope(self, prefix, sub) -> *Router — mount every route of sub under
  • 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 the
  • path key.

  • Query<T>Query::extract(req, "<param-name>") against the query string.
  • State<T>State::extract(req), reading the slot set by Router::state.
  • any other TT::from_request(req) via the FromRequest trait.

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.

BearerAuthorization: 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) -> booltrue when 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 for name; "" when absent.
  • has(self: *Form, name: string) -> booltrue when 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")));
}
pub struct Cookies {
    pub header: string,
}

Wraps the Cookie: request header (read-only).

  • get(self: *Cookies, name: string) -> string — value for name; "" when absent.
  • has(self: *Cookies, name: string) -> booltrue when 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 a T: JsonBind value.
  • json_respond<T: JsonBind>(v: T) -> HttpResponse — one-liner equivalent to Json::wrap(v).into_response(); emits a 200 with the JSON body and Content-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-encoded k=v&k=v wire string.

Free functions:

  • form_decode(s: string) -> !*FormData — parse a k=v&k=v body; 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) -> stringmultipart/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 every Set-Cookie line, scoped to request_host; same (name, domain, path) replaces.
  • header_value(self: *CookieJar, host: string, path: string, is_secure: bool) -> string — build the outbound Cookie: 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-Match304 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 (Glide string is 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 a ByteBuffer with explicit length. Binary-safe; use this for HPACK header blocks.
  • read_frame_header(raw: *ByteBuffer, pos: i32) -> !*FrameHeader — parse a 9-byte header at pos.
  • 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 to buf.

HpackDecoder:

  • HpackDecoder::new() -> *HpackDecoder — 4 KiB dynamic table cap.
  • decode_block(self, raw: string, len: i32) -> !*Vector<*HpackHeader> — decode a full header block. len is 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 handshaked h2 TLS 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-handshaked h2 TLS stream. Closes the stream when the connection ends; the caller does not call s.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_headers is a CRLF-joined Name: Value block.

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() -> *H3SessionCache
  • free(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 - accepted is 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 as http3_request but 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 covers DNS:localhost and IP:127.0.0.1), good for days days. 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 UDP port. A single C reader thread demuxes inbound packets across up to 64 connections; each accepted connection's request is dispatched to handler on 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.