Capítulo 28 95 min de leitura

Rede, HTTP & URL

O Glide entrega uma stack de rede completa como stdlib em espaço de usuário: primitivas de socket, todos os transportes comuns (TCP, UDP, multicast, raw, TLS, WebSocket), três gerações de cliente HTTP (1.1, 2, 3), um servidor HTTP/1.1 roteado e o codec de URL que os cola. Tudo é escrito em Glide sobre uma única superfície fina de syscalls de runtime (gnet_* em netcore.c); não há código C de protocolo escondido acima da camada de socket.

A I/O segue o modelo coro-blocking. Uma chamada como accept, read ou connect bloqueia a coroutine chamadora, não a thread do SO. No Linux o file descriptor é registrado no reactor epoll e a coroutine fica parqueada até o kernel sinalizar prontidão, então a thread worker fica livre para rodar outras coroutines no meio-tempo — um único processo pode segurar dezenas de milhares de conexões vivas. No Windows e no macOS a mesma superfície cai para I/O síncrona bloqueante: seu código é idêntico e portável, mas essas plataformas prendem um worker por chamada em voo até os backends assíncronos (IOCP / kqueue) chegarem. Você escreve código em linha reta; o scheduler faz a multiplexação.

Erros são valores. Quase toda chamada falível retorna !T (um Result), inspecionado através de .ok mais .val no sucesso ou .err (uma string) na falha — sem exceções, sem panics no caminho feliz-ou-triste. Cheque explicitamente com if !r.ok { ... }, ou propague com o operador ? dentro de uma função que ela mesma retorna !T. Construtores que não podem falhar (IpAddr::v4, HttpResponse::ok) retornam o valor diretamente; tudo que toca a rede ou faz parsing de entrada não confiável retorna !T.

Import

// Endereçamento central + fundação de socket
import stdlib::net::ip::*;          // IpAddr, SocketAddr
import stdlib::net::sys::*;         // Socket, Stream — a fundação gnet_*
import stdlib::net::sockopt::*;     // SO_REUSEADDR / TCP_NODELAY / timeouts
import stdlib::net::dns::*;         // resolve()

// Transportes
import stdlib::net::tcp::*;         // TcpStream (cliente)
import stdlib::net::listener::*;    // Listener, Conn (servidor)
import stdlib::net::udp::*;         // UdpSocket, UdpRecv
import stdlib::net::multicast::*;   // join/leave de multicast IPv4
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

// Cliente HTTP
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::*;         // framing HTTP/2 + cliente GET
import stdlib::http::hpack::*;      // compressão de header HPACK
import stdlib::http::h3::*;         // HTTP/3 sobre QUIC

// Servidor HTTP
import stdlib::http::*;             // HttpRequest, HttpResponse, http_listen
import stdlib::http::router::*;     // Router, Route, Chain
import stdlib::http::route::*;      // attrs @route / @get / @post, routes!()
import stdlib::http::handler::*;    // proc macro @handler
import stdlib::http::extract::*;    // FromRequest, Bearer, Path, Headers, Basic
import stdlib::http::typed::*;      // Json<T>, JsonBind, into_response()
import stdlib::http::cors::*;       // CorsConfig, middleware CORS
import stdlib::http::compress::*;   // compressão de resposta gzip_mw
import stdlib::http::static::*;     // serviço de arquivos estáticos
import stdlib::http::sse::*;        // Server-Sent Events
import stdlib::http::proxy::*;      // ReverseProxyConfig

// Codec de URL
import stdlib::url::*;              // url_encode, url_decode

Mapa de módulos

módulo o que te dá
stdlib::net::ip IpAddr (struct etiquetada IPv4/IPv6) e SocketAddr (ip+porta) — construir, fazer parsing, classificar (is_loopback/is_private/...) e renderizar para texto (RFC 5952).
stdlib::net::sys A fundação de socket de primeira classe: o handle Socket e o trait Stream binary-safe sobre as syscalls gnet_* do runtime. Todo transporte mais alto se constrói sobre isto.
stdlib::net::sockopt Helpers uniformes de opção de socket (SO_REUSEADDR, TCP_NODELAY, timeouts de recv/send) sobre qualquer .fd da fundação.
stdlib::net::dns Resolução hostname → IP via o resolver do sistema (getaddrinfo); resolve() retorna !*Vector<*IpAddr>.
stdlib::net::tcp TcpStream — connect TCP de saída (cliente) + read/write.
stdlib::net::listener TCP do lado servidor: Listener (bind/accept) e Conn (socket aceito), ligados ao reactor assíncrono.
stdlib::net::udp UdpSocket + UdpRecv — send_to / recv_from de datagramas sem conexão.
stdlib::net::multicast Multicast IPv4: join/leave de um grupo e ajuste do comportamento de envio multicast num socket UDP.
stdlib::net::raw RawSocket (SOCK_RAW) para payloads de camada IP. Precisa de root / CAP_NET_RAW / Administrador.
stdlib::net::icmp ping e traceroute, construídos em Glide puro sobre um raw socket.
stdlib::net::tls Cliente e servidor TLS 1.2 / 1.3 (TlsConfig, TlsStream) apoiados em OpenSSL; linka -lssl -lcrypto.
stdlib::net::ws Cliente WebSocket (RFC 6455) sobre ws:// (TCP) ou wss:// (TLS): WebSocket, WsMessage.
stdlib::http::client O cliente HTTP/1.1+2+3: HttpClient, HttpResponse e os helpers one-shot http_get / http_post_*.
stdlib::http::cookies CookieJar para o cliente — faz parsing de Set-Cookie, reenvia Cookie:.
stdlib::http::form FormData — builder + parser de corpo application/x-www-form-urlencoded.
stdlib::http::multipart Multipart — builder multipart/form-data para upload de arquivos.
stdlib::http::jwt JWT (JwtClaims) sign + verify sobre HMAC.
stdlib::http::h2 Framing HTTP/2 (RFC 7540) + o transporte cliente só-GET (habilite com c.http2 = true).
stdlib::http::hpack Compressão de header HPACK (RFC 7541) usada pelo transporte HTTP/2.
stdlib::http::h3 Cliente e servidor HTTP/3 sobre QUIC (libngtcp2 / libnghttp3); habilite com c.http3 = true.
stdlib::http O núcleo do servidor HTTP/1.1: HttpRequest, HttpResponse, o trait de streaming ChunkSource e http_listen / https_listen / http_listen_workers.
stdlib::http::router Router ciente de método (Router, Route, Chain) com segmentos :param e *wildcard e cadeias de middleware.
stdlib::http::route @route(METHOD, "/path") + attrs açucarados @get/@post/... e a macro de auto-registro routes!(r).
stdlib::http::handler Proc macro @handler — params de handler tipados ao estilo Axum sobre a forma fn-pointer do router.
stdlib::http::extract Extractors FromRequest: Bearer, Headers, Basic, Authorization<S>, Path<T>.
stdlib::http::typed Ergonomia de JSON tipado: Json<T>, o trait JsonBind e value.into_response().
stdlib::http::cors CorsConfig + middleware CORS para o router.
stdlib::http::compress gzip_mw — middleware de compressão gzip de resposta (linka -lz).
stdlib::http::static Middleware de arquivos estáticos: monta um diretório num prefixo de URL com ETag / 304 / index.html.
stdlib::http::sse Server-Sent Events (text/event-stream) em cima de ChunkSource.
stdlib::http::proxy ReverseProxyConfig — encaminha requisições para uma origem upstream.
stdlib::url Percent-encoding de URL: url_encode e url_decode (RFC 3986).

IP & endereços de socket — IpAddr

Uma única struct etiquetada guarda um endereço IPv4 ou IPv6. Não há rotatividade de heap além do único malloc por endereço: v4 empacota num campo de 32 bits, v6 em duas metades de 64 bits. SocketAddr junta um IpAddr com uma porta. O parsing é falível e retorna !T (um Result); leia o resultado através de .ok / .val / .err.

IpAddr

pub struct IpAddr {
    pub kind: i32,    // 4 ou 6
    pub v4: i32,      // endereço de 32 bits em ordem-de-host, válido quando kind == 4
    pub v6_hi: i64,   // 64 bits superiores, ordem-de-host, válido quando kind == 6
    pub v6_lo: i64,   // 64 bits inferiores, ordem-de-host, válido quando kind == 6
}

kind é 4 ou 6; os outros campos só têm significado para o kind correspondente. Para v4, 127.0.0.1 empacota em 0x7F000001. Para v6, ::1 tem v6_hi = 0, v6_lo = 1.

Construtores

  • IpAddr::v4(a: i32, b: i32, c: i32, d: i32) -> *IpAddr — a partir de quatro octetos.
  • IpAddr::v6(hi: i64, lo: i64) -> *IpAddr — a partir de duas metades de 64 bits (ordem-de-host).
  • IpAddr::loopback_v4() -> *IpAddr127.0.0.1.
  • IpAddr::loopback_v6() -> *IpAddr::1.
  • IpAddr::unspec_v4() -> *IpAddr0.0.0.0 (bind em qualquer 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

Predicados

  • is_v4(self: *IpAddr) -> booltrue se for um endereço v4.
  • is_v6(self: *IpAddr) -> booltrue se for um endereço v6.
  • is_loopback(self: *IpAddr) -> booltrue para 127.0.0.0/8 (v4) ou ::1 (v6).
  • is_unspecified(self: *IpAddr) -> booltrue para o endereço todo-zeros.
  • is_private(self: *IpAddr) -> booltrue para RFC 1918 (10/8, 172.16/12, 192.168/16) em v4 ou fc00::/7 unique-local em 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

Formatação & igualdade

  • to_string(self: *IpAddr) -> string — endereço textual; v6 segue a RFC 5952 (sequência de zeros mais longa colapsada em ::).
  • eq(self: *IpAddr, other: *IpAddr) -> bool — igualdade byte a byte. Um valor v4 e um v6 sempre comparam como diferentes mesmo quando se referem ao mesmo endereço.
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 — faz parse de "1.2.3.4" (v4) ou "::1" / "2001:db8::1" (v6). Retorna err para entrada malformada. A presença de um : seleciona o caminho v6.
let r: !*IpAddr = IpAddr::parse("192.168.0.1");
if !r.ok {
    println!("parse falhou:", 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 — junta um IP e uma porta.
  • to_string(self: *SocketAddr) -> string — renderiza como host:port; hosts v6 são envolvidos em colchetes conforme a RFC 3986.
  • SocketAddr::parse(s: string) -> !*SocketAddr — faz parse de "1.2.3.4:80" ou "[::1]:80".
  • free(self: *SocketAddr) — libera o IpAddr envolvido e a própria struct SocketAddr.
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"

Faça o round-trip de um endereço de socket textual, tratando o erro explicitamente:

let r: !*SocketAddr = SocketAddr::parse("127.0.0.1:8080");
if !r.ok {
    println!("endereço inválido:", r.err);
    return;
}
let sa: *SocketAddr = r.val;
defer sa.free();
println!(sa.port);                    // => 8080

SocketAddr::parse("[::1]:443").ok;    // => true

Resolução DNS — resolve

stdlib::net::dns transforma um hostname em uma lista de endereços IP envolvendo o resolver do sistema (getaddrinfo). Não há um tipo de registro próprio — uma consulta produz um *Vector<*IpAddr> reaproveitando o tipo IpAddr da seção de IP, com cada entrada carregando sua própria família v4/v6.

import stdlib::net::dns::*;

A API é síncrona: uma chamada bloqueia o coro chamador até o SO responder. O escalonador do runtime mantém os outros coros do mesmo worker rodando, então uma consulta lenta não trava o app inteiro, mas cada lookup ainda é uma syscall bloqueante. Cargas dominadas por resolução devem se espalhar entre coros via spawn / WaitGroup.

Modelo de erro

Todo ponto de entrada retorna !*Vector<*IpAddr> (um Result). Inspecione .ok antes de ler .val; em caso de falha .err guarda a mensagem:

let r: !*Vector<*IpAddr> = resolve("does-not-exist.invalid");
if !r.ok {
    println!(r.err);                 // => dns: resolve failed for does-not-exist.invalid
    return;
}

O único erro produzido pelo wrapper do resolver é dns: resolve failed for <host>, levantado quando getaddrinfo reporta qualquer falha. Uma chamada bem-sucedida ainda pode retornar um vetor vazio se o host não tiver registros A ou AAAA.

resolve

pub fn resolve(host: string) -> !*Vector<*IpAddr>

Resolve host para todos os registros A e AAAA que o resolver do sistema retorna, na ordem em que o resolver os retornou. Endereços v4 e v6 são misturados em um único vetor; teste cada um com 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.
    }
}

Usando ? para propagar o erro em vez de ramificar:

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>

Igual a resolve, mas mantém apenas registros v4 (A). O vetor resultante contém somente endereços onde is_v4() é verdadeiro.

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>

Igual a resolve, mas mantém apenas registros v6 (AAAA).

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 que resolve_v4 e resolve_v6 ambos chamam resolve internamente e filtram o resultado, então uma falha do resolver aparece de forma idêntica através de .err, e um vetor vazio significa que o host não anunciou registros daquela família.

TCP — cliente e servidor

Sockets de fluxo de bytes brutos. O lado cliente é o TcpStream (conecta para um host remoto); o lado servidor é um Listener que aceita Conns em loop. Ambos passam pelo reactor assíncrono do runtime: no Linux um coro estacionado libera seu worker enquanto o kernel completa o handshake ou preenche o buffer de leitura; no Windows/macOS/BSD a mesma superfície roda como I/O síncrono bloqueante.

import stdlib::net::tcp::*;
import stdlib::net::listener::*;
import stdlib::net::ip::*;

Para um servidor HTTP, use http_listen (veja a seção do servidor HTTP) em vez de montar à mão um loop de accept com Listener — ele faz parsing de requisição, keep-alive e o caminho rápido de writev header+body por você. As primitivas abaixo são para protocolos TCP simples.

TcpStream — cliente de saída

Um socket de stream conectado. O único campo é o file descriptor:

pub struct TcpStream {
    pub fd: i64,
}

TcpStream também implementa a trait Stream, então um valor pode ser mantido como *dyn Stream ao lado de um stream TLS e compartilhado por camadas superiores (ws, http).

TcpStream::connect(dst: *SocketAddr) -> !*TcpStream

Conecta a um endereço já resolvido. Retorna err se o socket não puder ser criado ou o handshake falhar.

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()); }   // => contagem de bytes da resposta

TcpStream::connect_host(host: string, port: i32) -> !*TcpStream

Resolve host via DNS e conecta ao primeiro endereço alcançável na port, tentando cada endereço retornado em ordem. Retorna o último erro de conexão se todos os candidatos falharem.

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

Escreve data uma vez. Pode enviar menos bytes que data.len() sob back-pressure; a contagem retornada é o que realmente saiu. Use write_all quando precisar que cada byte seja entregue.

let w: !i32 = s.write("PING\r\n");
if w.ok { println!(w.val); }   // => bytes escritos nesta chamada

stream.write_all(data: string) -> !i32

Escreve cada byte de data, fazendo loop no runtime até o payload ser drenado. Retorna o total escrito.

let r: !i32 = s.write_all("HELLO server\r\n");
if !r.ok { println!(r.err); }

stream.read(max: i32) -> !string

Lê até max bytes. ok com uma string não vazia é dado; ok com uma string de tamanho zero significa que o peer fechou a conexão de forma limpa; err é um erro de socket.

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

Lê repetidamente até EOF (o peer fecha), acumulando tudo. Útil para trocas no estilo HTTP/1.0 de "ler a resposta inteira".

s.write_all("GET / HTTP/1.0\r\n\r\n");
let r: !string = s.read_to_end();
if r.ok { println!(r.val); }   // corpo completo da resposta

stream.set_timeout_ms(ms: i32)

Aplica um timeout combinado de leitura + escrita em milissegundos via SO_RCVTIMEO / SO_SNDTIMEO. Passe 0 para desabilitar. O fd do cliente permanece bloqueante para o timeout fazer efeito.

s.set_timeout_ms(5000);        // deadline de leitura/escrita de 5s
let r: !string = s.read(4096); // err no timeout em vez de travar

Métodos da trait Stream

Como TcpStream implementa Stream, estes métodos de buffer binário também estão disponíveis:

  • stream.read_bytes(buf: *u8, max: i32) -> !i32 — lê até max bytes para o
  • buffer do chamador; retorna a contagem (0 = peer fechou).

  • stream.write_bytes(buf: *u8, n: i32) -> !i32 — escreve n bytes de buf.
  • stream.close(self) — fecha o socket e libera a struct. Após close() o
  • ponteiro fica pendurado.

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);   // ecoa os bytes de volta
}
free(buf as *void);

Listener — loop de accept do servidor

Listener envolve um socket bound e em escuta; accept devolve um Conn por cliente. Ambos carregam apenas um fd:

pub struct Listener { pub fd: i32 }
pub struct Conn     { pub fd: i32 }

Listener::bind(port: i32) -> *Listener

Faz bind e escuta na port em todas as interfaces. O Listener retornado tem fd < 0 em caso de falha (porta em uso, privilégios ausentes); sempre verifique ok() antes de aceitar.

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

Igual a bind, mas seta SO_REUSEPORT onde for suportado para que múltiplos processos possam compartilhar uma porta de accept e deixar o kernel balancear os accepts entre eles. No Windows isso degrada para o comportamento de bind comum (somente SO_REUSEADDR).

let l: *Listener = Listener::bind_reuseport(8080);
if !l.ok() { println!("bind failed"); return 1; }
defer l.close();

listener.ok(self) -> bool

true quando o listener está bound e pronto para aceitar.

listener.accept(self) -> *Conn

Bloqueia até um cliente conectar, retornando um Conn. O Conn tem fd < 0 em caso de erro — verifique ok() primeiro.

while true {
    let c: *Conn = l.accept();
    if !c.ok() { c.close(); continue; }
    spawn handle(c);          // um coro por conexão
}

listener.close(self)

Fecha o socket de escuta e libera a struct. Combine com defer.

Conn — uma conexão aceita

conn.ok(self) -> bool

true quando o handshake da conexão foi completado.

conn.read(buf: *void, max: i32) -> i32

Lê até max bytes para buf. Retorna a contagem de bytes, 0 quando o peer fechou de forma limpa, ou -1 em caso de erro.

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

Escreve n bytes de buf. Pode escrever menos que n — faça loop na contagem retornada para payloads maiores que o buffer de envio do kernel.

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

Conveniência: escreve uma string inteira (nenhum NUL ao final é enviado). Retorna a contagem de bytes escritos.

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 de dois buffers em uma única syscall writev / WSASend — envia um header e um corpo sem uma cópia extra ou uma segunda syscall. Retorna o total de bytes escritos.

conn.sendfile(path: string) -> i32

Transferência zero-copy de arquivo → socket: sendfile() no Linux, TransmitFile() no Windows, um loop de read+write no macOS/BSD. As páginas de disco vão direto do page cache do kernel para o buffer do socket. Retorna o total de bytes enviados (o tamanho do arquivo em caso de sucesso) ou -1 em caso de erro.

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)

Fecha a conexão e libera a struct. O ponteiro fica pendurado depois; não o toque novamente. Combine com defer.

Servidor de echo completo

Um servidor de echo de linha completo: bind, accept em loop, e spawn de um coro por conexão que lê e escreve de volta até o peer fechar.

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 ou erro
        c.write(buf as *void, n);    // ecoa os bytes direto de volta
    }
}

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

Sockets de datagrama sem conexão. Diferente do TCP não há fluxo nem handshake: todo send_to carrega seu destino e todo recv_from informa o remetente. A E/S de datagramas é bloqueante na v1. O socket assenta sobre a fundação net::sys, então um UdpSocket é apenas um descritor de arquivo encapsulado.

Quase toda chamada retorna um !T (Result); exponha-o via .ok / .err / .val, ou faça curto-circuito com ?.

UdpSocket

pub struct UdpSocket {
    pub fd: i64,
}

O fd cru é exposto porque os helpers de multicast (e qualquer opção de socket de baixo nível que você precise) operam diretamente sobre o descritor.

UdpSocket::bind(addr: *SocketAddr) -> !*UdpSocket

Liga um socket UDP a um endereço local. Passe IpAddr::unspec_v4() (ou a forma v6) para escutar em todas as interfaces. A família do endereço é escolhida a partir de addr.ip automaticamente.

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; }   // => texto da falha de bind
let sock: *UdpSocket = r.val;
defer sock.close();

UdpSocket::bind_port(port: i32) -> !*UdpSocket

Conveniência: liga em 0.0.0.0:port (todas as interfaces v4).

let sock: *UdpSocket = UdpSocket::bind_port(5000).val;
defer sock.close();

UdpSocket::send_to(self, data: string, dst: *SocketAddr) -> !i32

Envia data para dst. Retorna o número de bytes enviados. O UDP não fragmenta payloads de modo usuário, então mantenha cada chamada ≤ 1472 bytes para v4 seguro sobre 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

Bloqueia até um datagrama chegar. max limita o tamanho do payload — bytes além de max são descartados silenciosamente pelo kernel. O resultado emparelha o payload com o SocketAddr do remetente.

let r: !*UdpRecv = sock.recv_from(1500);
if !r.ok { println!(r.err); return 1; }
let pkt: *UdpRecv = r.val;
println!(pkt.data);              // => os bytes recebidos
println!(pkt.from.port);         // => porta de origem do remetente

UdpSocket::close(self)

Fecha o descritor e libera a struct. Combine com defer.

let sock: *UdpSocket = UdpSocket::bind_port(5000).val;
defer sock.close();

Um respondedor de eco completo, ligando em qualquer interface, lendo um datagrama e respondendo ao seu remetente:

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);         // ecoa de volta ao endereço de origem

UdpRecv

Um datagrama recebido: payload mais endereço do remetente.

pub struct UdpRecv {
    pub data: string,        // o payload do datagrama
    pub from: *SocketAddr,   // quem o enviou
}

from é reconstruído a partir do endereço de origem do kernel — datagramas IPv4 decodificam para um IpAddr em octetos pontilhados, IPv6 para um de 128 bits — então você pode responder com send_to(.., pkt.from) sem rastrear pares você mesmo.

Multicast

Adesão a grupo multicast IPv4 e ajuste de envio, sobrepostos a um UdpSocket ligado. Estas são funções livres que recebem o fd do socket em vez de métodos — ligue o socket primeiro, depois entre no grupo. O empacotamento do endereço em ordem de bytes de rede é tratado no runtime, então você passa IpAddrs em ordem do host.

mc_join_v4(fd: i64, group: *IpAddr, iface: *IpAddr) -> !i32

Entra em um group multicast IPv4 na interface local iface. Use IpAddr::unspec_v4() para a interface padrão. O socket já deve estar ligado.

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);   // agora recebe o tráfego do grupo
if r.ok { println!(r.val.data); }

mc_leave_v4(fd: i64, group: *IpAddr, iface: *IpAddr) -> !i32

Sai de um grupo multicast IPv4 ao qual já se aderiu.

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

Limite de saltos para datagramas multicast de saída. O padrão 1 mantém o tráfego na sub-rede local; aumente-o para rotear através de roteadores.

mc_set_ttl(sock.fd, 4);   // permite até 4 saltos de roteador

mc_set_loop(fd: i64, on: bool) -> !i32

Se o multicast enviado deste socket retorna aos membros locais (padrão ligado). Desligue quando não quiser receber seus próprios envios.

mc_set_loop(sock.fd, false);   // suprime o loopback local dos nossos próprios pacotes

Um remetente que entra em um grupo, desabilita o loopback para não ver seu próprio tráfego, e emite um datagrama com um TTL mais amplo:

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, cliente e servidor, apoiado em OpenSSL. A linha de link precisa de libssl + libcrypto — o Glide adiciona -lssl -lcrypto incondicionalmente, então um OpenSSL ausente aparece como o erro de link cannot find -lssl de praxe.

  • Windows: mingw-w64-ucrt-x86_64-openssl (toolchain UCRT64).
  • Linux / macOS: libssl-dev / openssl@3.

O SNI é definido automaticamente a partir do nome de host passado para connect, e o bundle de CA do sistema dirige a verificação. No Windows a cadeia de CA é ponteada a partir dos stores de sistema ROOT + CA via Crypt32, já que o build OpenSSL do mingw vem com um OPENSSLDIR vazio. Definir verify=false desabilita a validação de certificado — nunca faça isso em produção.

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

Configuração para uma conexão TLS de saída.

pub struct TlsConfig {
    pub verify: bool,      // valida cert + hostname do peer (sempre true em prod)
    pub min_proto: i32,    // 0=TLS1.0 1=TLS1.1 2=TLS1.2 3=TLS1.3 mínimo (padrão 2)
    pub alpn: string,      // lista ALPN separada por vírgula, ex. "h2,http/1.1"; "" desabilita
}
  • TlsConfig::client() -> *TlsConfig — config de cliente padrão: verify=true, mínimo TLS 1.2, sem ALPN.
  • TlsConfig::client_insecure() -> *TlsConfig — validação de cert desligada; só para dev local.
  • cfg.free() — libera a struct de config.

alpn e min_proto são campos simples, então ajuste-os após a construção:

let cfg: *TlsConfig = TlsConfig::client();
defer cfg.free();
cfg.alpn = "h2,http/1.1";   // anuncia HTTP/2 e depois HTTP/1.1
cfg.min_proto = 3;          // exige TLS 1.3

Config insegura contra um servidor de dev autoassinado:

let cfg: *TlsConfig = TlsConfig::client_insecure();   // NÃO use em prod
defer cfg.free();
let r: !*TlsStream = TlsStream::connect("localhost", 8443, cfg);
if r.ok { defer r.val.close(); /* fala com o servidor autoassinado */ }

TlsStream

Um stream TLS conectado. O único campo é o handle opaco do OpenSSL:

pub struct TlsStream {
    pub handle: *void,
}

TlsStream também implementa a trait Stream, então read_bytes / write_bytes / close ficam disponíveis ao lado dos métodos inerentes.

Construtores

  • TlsStream::connect(host: string, port: i32, cfg: *TlsConfig) -> !*TlsStream — resolve host via DNS, abre o TCP, roda o handshake. O hostname alimenta o SNI e a verificação de cert.
  • TlsStream::attach(tcp: *TcpStream, hostname: string, cfg: *TlsConfig) -> !*TlsStream — roda o handshake TLS sobre um TcpStream já conectado (upgrade estilo STARTTLS). Em caso de sucesso o stream retornado é dono do fd; não chame tcp.close() depois — o wrapper TcpStream é liberado para você.

I/O

  • stream.write(data: string) -> !i32 — escreve data; retorna os bytes escritos (pode ser parcial). Trunca em NUL.
  • stream.write_all(data: string) -> !i32 — escreve cada byte, fazendo loop em escritas parciais.
  • stream.read(max: i32) -> !string — lê até max bytes; ok("") significa que o peer fechou de forma limpa.
  • stream.read_to_end() -> !string — lê até o peer fechar a conexão.
  • stream.read_bytes(buf: *u8, max: i32) -> !i32 — leitura binária-segura para dentro de buf (trait Stream).
  • stream.write_bytes(buf: *u8, n: i32) -> !i32 — escrita binária-segura (trait Stream); ao contrário de write, não trunca em NUL.

Info de conexão

  • stream.alpn() -> string — protocolo ALPN negociado, ou "" se nenhum.
  • stream.version() -> i322 para TLS 1.2, 3 para TLS 1.3, 0 caso contrário.
  • stream.cipher() -> string — nome da suíte de cifra negociada, ex. TLS_AES_256_GCM_SHA384.
  • stream.set_timeout_ms(ms: i32) — aplica timeouts de leitura + escrita no socket subjacente.

Encerramento

  • stream.close() — encerra o TLS, fecha o socket, libera o stream (trait Stream).

Uma requisição que inspeciona a conexão negociada:

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

Upgrade STARTTLS — conecta em texto puro, negocia, depois promove a mesma conexão:

import stdlib::net::tcp::*;

let tcp: *TcpStream = TcpStream::connect_host("smtp.example.com", 587).val;
tcp.write_all("EHLO me\r\nSTARTTLS\r\n");
// ... lê a resposta "220 Go ahead" no 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 é consumido; não o feche
let s: *TlsStream = r.val;
defer s.close();
s.write_all("EHLO me\r\n");   // agora sobre TLS

Enquadramento binário-seguro sobre os métodos Stream:

let s: *TlsStream = TlsStream::connect("example.com", 443, TlsConfig::client()).val;
defer s.close();

let frame: *u8 = malloc(9) as *u8;   // payload de 9 bytes que pode conter NULs
// ... preenche o 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

Listener do lado servidor: mantém o SSL_CTX (cert + chave) mais o socket TCP de escuta. accept bloqueia até o próximo cliente e retorna um TlsStream assim que o handshake tem sucesso.

Construtores

  • TlsListener::bind(port: i32, cert_path: string, key_path: string) -> !*TlsListener — faz bind em 0.0.0.0:port, carrega o cert PEM (+ cadeia) e a chave privada PEM, começa a escutar.
  • TlsListener::bind_alpn(port, cert_path, key_path, alpn_csv: string) -> !*TlsListener — o mesmo, mas anuncia protocolos ALPN (CSV, ordem de preferência do servidor — a primeira oferta correspondente do cliente vence).

Aceitando

  • listener.accept() -> !*TlsStream — bloqueia até o próximo cliente, roda o handshake, retorna um stream pronto. Clientes cujo handshake falha são reportados como err.
  • listener.accept_raw() -> !i64 — accept só do TCP; retorna o fd bruto, deixando o handshake a cargo do chamador (tipicamente numa thread worker). Evita que o loop de accept bloqueie em um handshake lento ou silencioso.
  • listener.attach(fd: i64) -> !*TlsStream — completa o handshake em um fd vindo do accept_raw, reutilizando o contexto de cert do listener. O fd é fechado automaticamente em falha de handshake.

Encerramento

  • listener.close() — libera o contexto de cert e o socket de escuta.

Um loop de servidor mínimo estilo HTTPS:

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; }            // pula handshakes que falharam
    let s: *TlsStream = a.val;
    s.write_all("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi");
    s.close();
}

Anuncie HTTP/2 via ALPN e ramifique conforme o protocolo negociado:

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") {
        // fala HTTP/2
    } else {
        s.write_all("HTTP/1.1 200 OK\r\n\r\nhi");
    }
    s.close();
}

Descarregue o handshake para um worker para que um cliente travado não estacione o loop de accept:

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 fora do loop de accept
        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

Contraparte do lado servidor para TlsStream::attach: envolve uma conexão em texto puro já aceita em TLS para protocolos que promovem inline (SMTP STARTTLS, IMAP STARTTLS, POP3 STLS, FTPS AUTH TLS).

  • tls_attach_server(conn_fd: i32, cert_path: string, key_path: string) -> !*TlsStream

Em caso de sucesso o TlsStream retornado é dono do socket subjacente; não feche a conexão original depois.

// Num servidor SMTP, após o cliente enviar 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");   // o resto da sessão é criptografado

Sockets de baixo nível — Socket

Sob as conveniências de TCP/UDP/TLS há uma fundação de socket fina e tipada: stdlib::net::sys declara a superfície de syscalls gnet_* do runtime e envolve um fd com posse em Socket, num trait Stream binário-seguro e num conjunto plano de constantes de família / opção / shutdown. stdlib::net::sockopt adiciona os toggles comuns de opção sobre qualquer .fd; stdlib::net::raw abre sockets SOCK_RAW; e stdlib::net::icmp constrói ping / traceroute em Glide puro sobre um socket raw. Toda chamada retorna !T e expõe erros via .ok / .err (sem panics), então um privilégio ausente ou uma opção sem suporte é apenas um valor de erro.

Constantes

De stdlib::net::sys (cada uma é pub const … : i32):

// tipo de socket
SOCK_STREAM = 1   SOCK_DGRAM = 2   SOCK_RAW = 3
// família de endereço
AF_V4 = 4   AF_V6 = 6
// shutdown(how)
SHUT_RD = 0   SHUT_WR = 1   SHUT_RDWR = 2
// tags de opção de gnet_const() (resolvidas para números por OS em 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

As tags OPT_* são índices em gnet_const, que as mapeia para os números reais de opção da plataforma — passe-as para Socket::set_opt_int. De stdlib::net::raw, IPPROTO_ICMP = 1 é o número de protocolo IANA para um socket ICMP raw.

Socket

pub struct Socket {
    pub fd: i64,
}

Um único fd com posse. Socket implementa Stream, então é fechado através do close do trait (não há um close inerente separado).

Statics e métodos:

  • Socket::open(family: i32, ty: i32, proto: i32) -> !*Socket — abre um socket. family é AF_V4/AF_V6, ty é SOCK_STREAM/SOCK_DGRAM/SOCK_RAW, proto é 0 para o padrão.
  • bind(self: *Socket, sa: *SocketAddr) -> !i32 — faz bind a um endereço local.
  • connect(self: *Socket, sa: *SocketAddr) -> !i32 — conecta a um endereço remoto.
  • listen(self: *Socket, backlog: i32) -> !i32
  • accept(self: *Socket) -> !*Socket — aceita uma conexão, retornando um Socket novo com posse para o peer (integrado ao reactor; o socket aceito fica não-bloqueante + TCP_NODELAY).
  • local_addr(self: *Socket) -> !*SocketAddr — o endereço com bind local.
  • peer_addr(self: *Socket) -> !*SocketAddr — o endereço do peer conectado.
  • set_opt_int(self: *Socket, level: i32, opt: i32, val: i32) -> !i32 — define uma opção com valor int; level/opt vêm de gnet_const.
  • set_reuse_addr(self: *Socket, on: bool) -> !i32SO_REUSEADDR.
  • shutdown(self: *Socket, how: i32) -> !i32how é SHUT_RD/SHUT_WR/SHUT_RDWR.
  • send(self: *Socket, buf: *void, n: i32) -> !i32 — envia n bytes; retorna os bytes enviados.
  • recv(self: *Socket, buf: *void, max: i32) -> !i32 — recebe até max bytes; retorna os bytes lidos (0 no EOF).
import stdlib::net::sys::*;
import stdlib::net::ip::*;

// Abrir, fazer bind e listen num socket de stream à mão.
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

O trait de transporte binário-seguro. Socket o implementa, e os streams de TLS/raw compartilham o mesmo formato, então código genérico pode receber um *dyn Stream.

pub trait Stream {
    fn read_bytes(self: *Self, buf: *u8, max: i32) -> !i32;   // bytes lidos (0 = EOF)
    fn write_bytes(self: *Self, buf: *u8, n: i32) -> !i32;    // bytes escritos
    fn close(self: *Self);
}

Duas funções livres envolvem um *dyn Stream para I/O em formato de string:

  • stream_read(s: *dyn Stream, max: i32) -> !string — lê até max bytes como uma string.
  • stream_write_all(s: *dyn Stream, data: string) -> !i32 — escreve uma string inteira; retorna os bytes escritos.
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 libera o Socket

Opções de socket — sockopt

stdlib::net::sockopt dá uma superfície uniforme sobre qualquer fd da fundação (um Socket, TcpStream ou UdpSocket — todos expõem .fd). Cada uma recebe o fd e retorna !i32. Uma opção sem suporte na plataforma atual (ex.: SO_REUSEPORT fora do Linux) retorna um erro em vez de ter sucesso silenciosamente.

  • sock_set_reuse_addr(fd: i64, on: bool) -> !i32SO_REUSEADDR, refaz bind numa porta recém-fechada imediatamente.
  • sock_set_reuse_port(fd: i64, on: bool) -> !i32SO_REUSEPORT, balanceia accepts entre workers (Linux).
  • sock_set_nodelay(fd: i64, on: bool) -> !i32TCP_NODELAY, desabilita o Nagle para que escritas pequenas sejam enviadas de uma vez.
  • sock_set_broadcast(fd: i64, on: bool) -> !i32SO_BROADCAST, permite enviar a um endereço de broadcast (UDP).
  • sock_set_ttl(fd: i64, ttl: i32) -> !i32IP_TTL, limite de hops unicast de saída.
  • sock_set_recv_buf(fd: i64, bytes: i32) -> !i32 — dica de tamanho de SO_RCVBUF.
  • sock_set_send_buf(fd: i64, bytes: i32) -> !i32 — dica de tamanho de SO_SNDBUF.
  • sock_set_timeout_ms(fd: i64, ms: i32) -> !i32 — timeout de leitura + escrita em ms (0 desabilita); apoiado por SO_RCVTIMEO/SO_SNDTIMEO. Só atua num fd bloqueante.
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); }   // fora do Linux: imprime o erro
sock_set_timeout_ms(so.fd, 5000)?;                        // limita leituras bloqueantes a 5s

Sockets raw — RawSocket

stdlib::net::raw abre sockets SOCK_RAW para payloads de camada IP (ICMP, protocolos customizados, inspeção de pacotes). Sockets raw exigem privilégio — root / CAP_NET_RAW no Linux+macOS, Administrator no Windows — então open_* retorna um erro (tipicamente "1:Operation not permitted") quando o processo não tem permissão.

pub struct RawSocket {
    pub fd: i64,
}

// um pacote recebido (inclui o cabeçalho IP num recv de SOCK_RAW)
pub struct RawRecv {
    pub data: string,
    pub n: i32,
    pub from: *SocketAddr,
}
  • RawSocket::open_ip(proto: i32) -> !*RawSocket — abre um socket IPv4 raw para proto (ex.: IPPROTO_ICMP).
  • RawSocket::open_icmp() -> !*RawSocket — abre um socket ICMP raw (o caso comum).
  • send_to(self: *RawSocket, buf: *void, n: i32, dst: *SocketAddr) -> !i32 — envia n bytes para dst; a camada IP adiciona o cabeçalho. Retorna os bytes enviados.
  • recv_from(self: *RawSocket, buf: *void, max: i32) -> !*RawRecv — recebe um pacote num buffer com posse do chamador; retorna a contagem de bytes (.n) e o remetente (.from). Bloqueante.
  • set_ttl(self: *RawSocket, ttl: i32) -> !i32 — limite de hops de saída (usado pelo traceroute).
  • set_timeout_ms(self: *RawSocket, ms: i32) -> !i32 — timeout de recepção em ms (0 desabilita).
  • 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 monta mensagens de echo ICMP byte a byte (cabeçalho + checksum de complemento-de-um) sobre um socket raw — nenhum C além da fundação. Como os sockets raw, estes precisam de privilégio e retornam err(...) quando não permitidos.

  • icmp_checksum(buf: *u8, n: i32) -> i32 — o checksum da Internet (RFC 1071). Sobre um pacote completo cujo campo de checksum está preenchido, retorna 0 (a checagem de validade).
  • icmp_build_echo(buf: *u8, id: i32, seq: i32, payload_len: i32) -> i32 — monta uma echo-request em buf (precisa de 8 + payload_len bytes); retorna o comprimento total.
  • ping(host: string, timeout_ms: i32) -> !i64 — pinga host uma vez; retorna o round-trip time em milissegundos.
  • traceroute(host: string, max_hops: i32, timeout_ms: i32) -> !*Vector<string> — sonda TTL 1..max_hops; uma linha por hop, "<ttl> <ip>" para um roteador que responde ou "<ttl> *" em timeout. Para no echo reply do destino. (Hops intermediários são só Linux/macOS; no Windows os replies de time-exceeded dos roteadores não chegam a um socket raw, então os hops aparecem como * até o reply final.)
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); }                   // ex.: 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 é um cliente WebSocket RFC 6455 sobre TCP puro (ws://) ou TLS (wss://). A V1 cobre o caso comum: frames de texto + binários, ping/pong e fechamento limpo. Extensões (permessage-deflate, múltiplos subprotocolos) estão fora de escopo, e payloads de entrada têm um teto para manter os limites de buffer controlados. Ainda não há uma API de accept no lado do servidor — este módulo conecta, ele não escuta.

import stdlib::net::ws::*;

Toda chamada falível retorna !T, exposta através de .ok / .val / .err. Frames do cliente são sempre mascarados, conforme RFC 6455 §5.3 — a biblioteca cuida do mascaramento para você.

WsOp — opcodes de frame

WsOp é um namespace de constantes de opcode (RFC 6455 §11.8). Compare-as contra WsMessage.kind.

Static Valor Significado
WsOp::cont() 0 Frame de continuação (mensagens fragmentadas)
WsOp::text() 1 Frame de texto
WsOp::binary() 2 Frame binário
WsOp::close() 8 Frame de fechamento (WsMessage.close_code carrega o motivo)
WsOp::ping() 9 Frame de ping
WsOp::pong() 10 Frame de pong
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

Um frame de entrada, retornado por WebSocket::recv.

pub struct WsMessage {
    pub kind: i32,        // um de WsOp::text() / binary() / close() / ping() / pong()
    pub text: string,     // payload (frames de texto carregam a string; binários reusam este campo)
    pub close_code: i32,  // preenchido quando kind == WsOp::close(), senão 0
}

Ainda não há um tipo dedicado de bytes, então payloads binários também chegam em text.

WsParsedUrl

A forma parseada de uma URL ws:// / wss://.

pub struct WsParsedUrl {
    pub is_wss: bool,    // true para wss://, false para ws://
    pub host:   string,  // host sem porta
    pub port:   i32,     // porta explícita, senão 80 (ws) / 443 (wss)
    pub path:   string,  // caminho da requisição, padrão "/"
}

host_header(self) -> string

Formata o host para o header HTTP Host, descartando a porta quando ela coincide com o padrão do esquema (80 para ws, 443 para wss).

// (valores de WsParsedUrl vêm de WebSocket::connect internamente; mostrado pela forma)
// host_header() => "example.com:8443" quando uma porta não-padrão está definida,
//               => "example.com"      quando a porta é o padrão do esquema

WebSocket

A conexão do cliente. Obtenha uma com WebSocket::connect; ela possui um *dyn Stream sobre um TcpStream (ws://) ou um TlsStream (wss://), então nenhuma ramificação de transporte vaza para o seu código.

WebSocket::connect(url) -> !*WebSocket

Abre uma conexão e realiza o handshake de HTTP Upgrade. url deve ser ws://host[:port]/path ou wss://host[:port]/path. A chamada verifica que o servidor retornou 101 Switching Protocols e que Sec-WebSocket-Accept corresponde a base64(sha1(key + magic)); qualquer divergência retorna um 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

Envia um frame de texto UTF-8 (opcode 1). Sempre mascarado.

let w: !i32 = ws.send_text("{\"type\":\"hello\"}");
if !w.ok { println!("send failed:", w.err); }

send_binary(self, data) -> !i32

Envia um frame binário (opcode 2). Os bytes do payload ficam em uma string.

ws.send_binary("\x00\x01\x02\x03");

pong(self, payload) -> !i32

Envia um Pong (opcode 10), tipicamente ecoando um payload de Ping recebido para manter a conexão viva.

let m: !*WsMessage = ws.recv();
if m.ok && m.val.kind == WsOp::ping() {
    ws.pong(m.val.text);   // ecoa o payload do ping de volta
}

recv(self) -> !*WsMessage

Lê um frame de entrada. Pings são retornados literalmente — o chamador decide se faz auto-pong. Frames de fechamento aparecem com kind == WsOp::close() e close_code preenchido. Payloads mascarados são desmascarados para você.

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) -> ()

Envia um frame de fechamento e desmonta o transporte. code 1000 é "fechamento normal"; veja RFC 6455 §7.4 para o registro. Isso libera a conexão — não use ws depois disso.

ws.close(1000, "bye");          // fechamento normal
ws.close(4000, "app reset");    // código definido pela app (4000–4999)

Cliente completo: connect, send, loop de recv, close

Um driver completo de cliente echo. Ele conecta, envia uma mensagem, depois faz loop em recv, fazendo auto-pong nos pings e quebrando em um frame de fechamento.

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;
}

Cliente HTTP — HttpClient

Um cliente HTTP/1.1 e HTTP/2 bloqueante. HttpClient guarda a configuração reutilizável (user-agent, política de redirecionamento, cookie jar, timeout, verificação de TLS, opt-in de HTTP/2); HttpClientRequest é o envelope mutável da requisição que você monta e entrega a ele. Tanto http:// quanto https:// são suportados — HTTPS roda sobre stdlib::net::tls::TlsStream, e quando http2 está habilitado o cliente tenta h2 via ALPN e cai de volta para HTTP/1.1 de forma transparente.

Toda chamada retorna !*HttpResponse (um Result): teste .ok, leia o valor em .val, a mensagem em .err. O tipo da resposta é HttpResponse de stdlib::http — seus acessores (.status, .body, .header(name), .headers(name)) estão documentados na seção do servidor HTTP.

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

A requisição sendo montada. Um simples contêiner mutável — distinto do HttpRequest do lado servidor, que é parseado do fio.

pub struct HttpClientRequest {
    pub method:  string,
    pub url:     string,
    pub headers: string,   // linhas cruas "Name: Value\r\n"
    pub body:    string,
}
  • pub fn new(method: string, url: string) -> *HttpClientRequest — monta um
  • envelope com headers e body vazios.

  • pub fn set(self, name: string, value: string) — adiciona uma linha de header.
  • Múltiplas chamadas adicionam múltiplas linhas; não há deduplicação, então last-wins é responsabilidade de quem chama.

  • pub fn body_str(self, s: string) — define o body e o header
  • Content-Length correspondente em um único passo.

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

Configuração persistente do cliente. Mantenha uma instância por perto para compartilhar padrões entre várias requisições; para chamadas únicas as funções livres http_get / http_post_json montam um cliente padrão por chamada.

pub struct HttpClient {
    pub user_agent:   string,   // enviado como User-Agent; padrão "glide/0.1"
    pub max_redirects: i32,     // saltos que `do` vai seguir; padrão 5
    pub jar:          *CookieJar, // null = sem tratamento de cookies
    pub tls_insecure: bool,     // pula verificação de certificado (só dev); padrão false
    pub timeout_ms:   i32,      // timeout total por requisição; 0 = desabilitado
    pub http2:        bool,     // tenta h2 sobre TLS via ALPN; padrão false
}

Semântica dos campos:

  • user_agent — valor do header de requisição User-Agent.
  • max_redirects — número máximo de saltos de redirecionamento que do segue
  • antes de retornar err("http_client: too many redirects"). do_once ignora isso.

  • jar — atribua um CookieJar::new() para habilitar a ingestão automática de
  • Set-Cookie mais a anexação do header Cookie: entre requisições. Permanece null (sem tratamento de cookies) por padrão.

  • tls_insecure — quando true, pula a validação do certificado TLS. Apenas
  • para dev local; código de produção DEVE deixar isso desligado.

  • timeout_ms — timeout total por requisição em milissegundos (connect + send +
  • receive), aplicado via timeouts de send/receive do socket. 0 o desabilita.

  • http2 — tenta HTTP/2 sobre TLS via ALPN, caindo de volta para HTTP/1.1
  • quando o servidor não negocia h2. Não tem efeito em URLs http:// (o upgrade h2c não está implementado).

Métodos:

  • pub fn new() -> *HttpClient — constrói com os padrões acima.
  • pub fn do(self, req: *HttpClientRequest) -> !*HttpResponse — envia e segue
  • redirecionamentos até max_redirects. Um 303 rebaixa o método para GET e descarta o body; 301/302 fazem o mesmo para métodos diferentes de GET/HEAD; 307/308 preservam método e body. Valores Location relativos são resolvidos contra a URL atual.

  • pub fn do_once(self, req: *HttpClientRequest) -> !*HttpResponse — envia
  • exatamente uma requisição, nunca seguindo redirecionamentos. Use para proxies e crawlers que precisam preservar respostas 30x verbatim.

  • pub fn get(self, url: string) -> !*HttpResponse — GET único via do.
  • pub fn post(self, url: string, body: string, content_type: string) -> !*HttpResponse
  • — POST com um Content-Type explícito.

  • pub fn post_json(self, url: string, body: string) -> !*HttpResponse
  • POST com Content-Type: application/json. Quem chama serializa o JSON.

  • pub fn put(self, url: string, body: string, content_type: string) -> !*HttpResponse
  • — PUT com um Content-Type explícito.

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

Cliente configurado com headers customizados e tratamento de erro explícito:

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 de JSON e leitura do status, de um header e do body da resposta:

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"}
}

Seguindo redirecionamentos com um timeout e um cookie jar. Atribuir um jar captura todo Set-Cookie e reenvia o header Cookie: correspondente em requisições posteriores; timeout_ms limita cada salto; max_redirects limita a cadeia:

import stdlib::http::cookies::*;

let c: *HttpClient = HttpClient::new();
c.jar = CookieJar::new();   // persiste cookies entre requisições
c.timeout_ms = 5000;        // 5s por requisição
c.max_redirects = 10;       // segue até 10 saltos

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; }

// O cookie de sessão definido acima agora é enviado automaticamente e qualquer
// redirecionamento 30x de /dashboard é seguido de forma transparente.
let page: !*HttpResponse = c.get("https://example.com/dashboard");
if page.ok { println!(page.val.status); }   // => 200

Para inspecionar um redirecionamento em vez de segui-lo, 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

Defina http2 = true para negociar h2 via ALPN em conexões HTTPS. A API permanece inalterada — as respostas voltam no mesmo formato HttpResponse — e o cliente cai de volta para HTTP/1.1 silenciosamente quando o servidor recusa 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 (sobre h2 se oferecido)

ParsedUrl

A forma parseada de uma URL de requisição. Produzida internamente pelo cliente; exposta para quem precisa da sua formatação de header Host:.

pub struct ParsedUrl {
    pub scheme: string,   // "http" ou "https"
    pub host:   string,
    pub port:   i32,      // padrão 80 (http) / 443 (https)
    pub path:   string,   // sempre começa com "/"
}
  • pub fn host_header(self) -> string — formata o valor do header Host:,
  • omitindo a porta quando ela é o padrão do scheme (80 para http, 443 para https) e mantendo-a caso contrário (ex.: example.com:8443).

Funções livres

Helpers de uso único que montam um HttpClient padrão por chamada. Para múltiplas requisições ou estado compartilhado (cookies, timeouts), construa HttpClient::new() uma vez e reutilize-o.

  • pub fn http_get(url: string) -> !*HttpResponse — GET com um cliente padrão.
  • pub fn http_post_json(url: string, body: string) -> !*HttpResponse — POST de
  • JSON com um cliente padrão.

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":"..."}
}

Servidor HTTP — HttpResponse

Um servidor HTTP/1.1 mínimo construído sobre o par assíncrono Listener / Conn. Um handler é apenas um fn(*HttpRequest) -> HttpResponse; os pontos de entrada recebem uma porta e esse handler. No Linux cada conexão roda numa coroutine estacionada, então um processo segura dezenas de milhares de clientes; no Windows / macOS / BSD o loop é serial (uma conexão por vez) até os reactors IOCP / kqueue chegarem.

import stdlib::http::*;

fn root(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().body("hello, ".concat(req.path));
}

fn main() -> i32 {
    http_listen(8080, root);   // nunca retorna em caso de sucesso
    return 0;
}

Limitações da v1: apenas HTTP/1.1 neste caminho (HTTP/2 e HTTP/3 têm seus próprios pontos de entrada), um único handler para todo path (o roteamento fica por cima — veja a seção do router), e o corpo é totalmente bufferizado antes de o handler rodar (sem uploads em streaming).

HttpRequest

Uma requisição de entrada, totalmente bufferizada. O tempo de vida do ponteiro é a chamada do handler — não o guarde.

pub struct HttpRequest {
    pub method:        string,
    pub path:          string,
    pub version:       string,
    pub headers_block: string,   // texto de fio cru "Name: Value\r\n…"
    pub body:          string,
    pub params:        *HashMap<string>,   // params de path do router
    pub queries:       *HashMap<string>,   // mapa de query parseado preguiçosamente
    pub tls:           bool,               // true se chegou via https_listen
    pub headers_cache: *HashMap<string>,   // cache preguiçoso de lookup de header
}

Getters:

  • header(self: *HttpRequest, name: string) -> string — lookup de header
  • case-insensitive, "" quando ausente. A primeira chamada parseia headers_block para um cache; chamadas posteriores são O(1). Headers repetidos mantêm o primeiro valor.

  • param(self: *HttpRequest, name: string) -> string — parâmetro de path do
  • router, "" quando desconhecido ou despachado sem um router.

  • query(self: *HttpRequest, name: string) -> string — valor decodificado da
  • query-string, "" quando ausente. Parseado + cacheado no primeiro acesso.

  • path_only(self: *HttpRequest) -> stringpath com o segmento de query ?...
  • removido.

  • is_method(self: *HttpRequest, m: string) -> bool — checagem de método
  • case-insensitive.

  • body_json(self: *HttpRequest) -> !*JsonValue — parseia o corpo como JSON;
  • err num corpo vazio ou inválido.

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");          // ex.: "localhost:8080"
    let q:    string = req.query("q");              // "" quando não há ?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

A resposta de saída. Construa com um construtor, depois encadeie os setters — cada setter retorna um HttpResponse novo (semântica de valor, não um ponteiro mutável).

pub struct HttpResponse {
    pub status:        i32,
    pub headers_block: string,
    pub body:          string,
    pub stream_src:    *dyn ChunkSource,   // null para respostas bufferizadas
    pub file_path:     string,             // não-vazio ⇒ caminho de sendfile
}

Construtores (estáticos):

  • HttpResponse::with_status(code: i32) -> HttpResponse — status dado, sem
  • corpo, sem headers.

  • HttpResponse::ok() -> HttpResponse — 200, sem corpo.
  • HttpResponse::not_found() -> HttpResponse — 404.
  • HttpResponse::redirect(url: string) -> HttpResponse — 302 com
  • Location: url.

  • HttpResponse::file(path: string, mime: string) -> HttpResponse — serve
  • um arquivo do disco via sendfile zero-copy; Content-Length é o tamanho do arquivo, mime preenche Content-Type.

Setters de corpo + content-type (cada um encadeável, retorna HttpResponse):

  • body(self, s: string) — define o corpo e o Content-Length correspondente.
  • text(self, s: string) — corpo + Content-Type: text/plain; charset=utf-8.
  • html(self, s: string) — corpo + Content-Type: text/html; charset=utf-8.
  • json(self, s: string) — corpo + Content-Type: application/json.
  • json_of(self, v: *JsonValue) — serializa v.emit() como JSON.
  • stream(self, source: *dyn ChunkSource) — emite
  • Transfer-Encoding: chunked, percorrendo source.next_chunk() até none(); body é ignorado quando uma fonte de stream é definida.

Setters de header + status:

  • status(self, code: i32) — substitui o código de status.
  • set(self, name, value: string) — define um header, substituindo qualquer linha
  • existente de mesmo nome (case-insensitive). CR/LF em name/value é removido.

  • add(self, name, value: string) — anexa uma linha de header, preservando
  • linhas de mesmo nome (para Set-Cookie, Vary, Link).

  • cookie(self, name, value: string) — anexa um header Set-Cookie: name=value
  • (passa por add).

Getters de header (recebem *HttpResponse):

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

  • headers(self: *HttpResponse, name: string) -> *Vector<string> — toda
  • linha de header correspondente (para 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

Um handler que retorna JSON, construído de duas formas:

fn api(req: *HttpRequest) -> HttpResponse {
    // 1. string JSON escrita à mão
    if req.path_only().eq("/ping") {
        return HttpResponse::ok().json("{\"ok\":true}");
    }
    // 2. montada via JsonValue, serializada por 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 — corpos em streaming

Uma resposta pode transmitir seu corpo pedaço a pedaço em vez de bufferizá-lo. Implemente a trait ChunkSource numa struct que segura o estado do cursor; o servidor emite um frame chunked por next_chunk() e termina em 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);
}

Pontos de entrada

http_listen(port: i32, handler: fn(*HttpRequest) -> HttpResponse) -> !i32 faz o bind, imprime uma linha de escuta e roda em loop para sempre. Retorna err("bind failed") quando a porta não pode ser bindada; nunca retorna em caso de sucesso.

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 é o irmão com TLS. cert_path / key_path são arquivos PEM. Ele anuncia h2,http/1.1 sobre ALPN — clientes com suporte a h2 negociam HTTP/2 de forma transparente, todos os outros caem para HTTP/1.1. req.tls é true dentro do 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;
}

Variantes multi-worker

http_listen_workers(port, n, handler) -> !i32 é o caminho rápido padrão: n workers pthread, cada um com seu próprio loop epoll + máquina de estados em C, com o handler invocado inline na thread do worker (sem spawn de coroutine). Na máquina de referência de 4 núcleos ele atinge ~159k req/s (~84% do Axum/Tokio no hello-world). Os handlers precisam ser não-bloqueantes — sem send/recv de chan, sem sleep_ms, sem I/O que estaciona; caso contrário o runtime aborta com uma mensagem apontando para a variante bloqueante. O caminho rápido é só Linux; outras plataformas roteiam automaticamente para a variante bloqueante.

http_listen_workers_blocking(port, n, handler) -> !i32 faz spawn de uma coro por conexão, então ops bloqueantes dentro do handler (queries de DB, HTTP downstream, sync de canal) estacionam apenas a coro daquela conexão. ~107k req/s na mesma máquina, handlers sem restrições.

fn root(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().body("hello, ".concat(req.path));
}

fn main() -> i32 {
    http_listen_workers(8080, 4, root);          // 4 workers, caminho rápido
    return 0;
}

Caminho de máquina de estados @leaf

Para os endpoints mais quentes há um caminho sem coroutine onde o handler roda inline na thread epoll do worker. A assinatura do handler é fn(string, string) -> string (método, path → corpo) e precisa ser marcada com @leaf (sem estacionar — o runtime aborta se estacionar). O framing embrulha o corpo retornado numa resposta 200 OK text/plain. ~70% mais rápido que http_listen_workers no hello-world; só Linux.

http_listen_sm_workers(port, n, h: fn(string, string) -> string) -> !i32 é a variante com handler de usuário:

@leaf
fn hello(method: string, _path: string) -> string {
    return "hello\n";
}

fn main() -> i32 {
    http_listen_sm_workers(8080, 4, hello);
    return 0;
}

Duas baselines de bench completam a família (sem handler de usuário — sempre respondem com um corpo hello-world fixo):

  • http_listen_sm_hello(port: i32) -> !i32 — single-worker.
  • http_listen_sm_hello_workers(port: i32, n: i32) -> !i32n workers,
  • cada um com seu próprio listener SO_REUSEPORT.

Helpers de nível mais baixo

  • parse_http_request(raw: string) -> !HttpRequest — parseia uma requisição
  • HTTP/1.1 completa a partir de bytes crus; err numa requisição malformada ou parcial.

  • format_http_response(r: *HttpResponse) -> string — renderiza uma resposta
  • para bytes de fio (sempre Connection: close). Exposta para que respostas possam ser encanadas para um logger ou 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"));

Roteamento & handlers — Router

Router casa o método + caminho da URL de uma requisição de entrada contra uma tabela de padrões declarados antecipadamente e despacha o primeiro casamento. Os padrões suportam três formas de segmento:

  • /usersliteral, precisa casar byte a byte.
  • /users/:idparam, captura um segmento, lido via req.param("id").
  • /static/*restwildcard, captura o resto do caminho (zero ou mais
  • segmentos) em req.param("rest"). Um wildcard só pode aparecer por último.

O despacho por método é exato (GETPOST) e insensível a maiúsculas no registro; registre o mesmo padrão duas vezes para dois verbos, ou use any(...) para um catch-all.

Importe tanto os tipos http quanto o router:

import stdlib::http::*;
import stdlib::http::router::*;

Constantes de tipo de segmento

pub const RS_LITERAL:  i32 = 0;   // segmento de texto literal
pub const RS_PARAM:    i32 = 1;   // captura `:name`
pub const RS_WILDCARD: i32 = 2;   // captura `*name` do resto do caminho
pub const ANY_METHOD: string = "*";   // verbo sentinela para `any(...)`

RouteSegment

Uma parte compilada de um padrão de rota.

pub struct RouteSegment {
    pub kind: i32,      // RS_LITERAL | RS_PARAM | RS_WILDCARD
    pub text: string,   // texto literal, ou o nome da captura para param/wildcard
}

Route

Uma linha da tabela de roteamento.

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 é a cadeia de middleware por rota, preenchida quando a rota foi montada através de Router::scope(prefix, sub). O middleware pai roda primeiro, depois local_mws na ordem de registro, depois o handler.

Router

pub struct Router {
    pub routes:    *Vector<Route>,
    pub mw:        *Vector<fn(*HttpRequest, *Chain) -> HttpResponse>,
    pub not_found: fn(*HttpRequest) -> HttpResponse,
}

As rotas são armazenadas na ordem de declaração; o primeiro casamento vence. mw roda antes do handler casado na ordem de registro. not_found cobre um despacho sem casamento e tem por padrão um 404 simples com corpo "not found".

Construção & registro

  • Router::new() -> *Router — router vazio com o handler 404 padrão.
  • route(self, method, pattern, handler) — registra handler para
  • method + pattern. O método é armazenado em maiúsculas; passe ANY_METHOD (ou "*") para todos os verbos.

  • get / post / put / delete / patch / head / options (self, pattern, handler)
  • — atalhos de verbo para route(...).

  • any(self, pattern, handler) — casa todo método HTTP em pattern.
  • route_with(self, method, pattern, mws, handler) — como route mas com uma
  • lista de middleware por rota (o mw no nível do router roda primeiro, depois mws, depois o handler).

  • not_found_with(self, handler) — sobrescreve o handler 404 padrão.
  • free(self) — libera a tabela de rotas e a struct do router; combine com
  • defer.

Um handler tem a forma fn(*HttpRequest) -> HttpResponse. Leia os parâmetros capturados com 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");          // capturado de `: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;
}

Uma rota wildcard captura tudo após o prefixo:

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");      // casa GET/POST/HEAD/...
});

dispatch rejeita qualquer caminho contendo um segmento .. com um 400 bad path antes de consultar a tabela, então um handler de servidor de arquivos com wildcard não consegue ler fora da sua raiz.

dispatch

dispatch(self, req) -> HttpResponse percorre a tabela, e no primeiro casamento preenche req.params, roda a cadeia de middleware, depois chama o handler. Sem casamento ele roda o handler de not-found. Ele muta req.params no lugar, então passe uma requisição recém-construída.

let req: HttpRequest = ...;
let resp: HttpResponse = r.dispatch(&req);     // => HttpResponse do handler

Estado da aplicação

state(self, p: *void) -> *Router armazena um ponteiro de estado da aplicação para todo o processo. Os handlers o alcançam através do extractor State<T> sem captura de closure. A v1 tem um único slot global. Retorna self para encadeamento.

let r: *Router = Router::new();
r.state(db as *void);                          // db: *Db
r.get("/users/:id", get_user);

Servindo

  • listen(self, port) -> !i32 — loop de coroutine-por-conexão. No Linux usa o
  • reactor + coroutines por conexão; Windows / macOS / BSD rodam serialmente até seus reactors chegarem.

  • listen_workers(self, port, n) -> !i32 — roteia através do servidor HTTP em C
  • com máquina de estados para máxima vazão. Os handlers PRECISAM ser não-bloqueantes (sem chan.send/recv, sem sleep_ms, sem I/O com parking).

  • listen_workers_blocking(self, port, n) -> !i32 — coroutine-por-conexão
  • através de n workers (balanceado por SO_REUSEPORT onde suportado) para handlers que genuinamente precisam bloquear.

Todos os três retornam !i32; uma falha de bind surge no braço err:

let r: *Router = Router::new();
r.get("/", root);
let res: !i32 = r.listen(8080);
if !res.ok { println!("listen failed: ", res.err); }

Middleware & grupos de rotas — Chain

O middleware tem a forma fn(*HttpRequest, *Chain) -> HttpResponse. Cada um ou chama chain_next(c) para avançar, ou retorna sem chamá-lo para curto-circuitar (o resto da cadeia e o handler da rota são pulados).

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) roda o próximo middleware, ou o handler da rota quando a lista se esgota. A resposta retornada pode ser inspecionada ou mutada antes do middleware retorná-la (comportamento estilo "after").

  • use_mw(self, mw) -> *Router — registra um middleware no nível do router; roda
  • antes do handler na ordem de registro. Retorna self para encadeamento.

  • scope(self, prefix, sub) -> *Router — monta toda rota de sub sob
  • prefix. O mw do sub-router vira middleware por rota que roda após a cadeia do pai e antes de cada handler. Copia as entradas de rota, então sub pode ser descartado ou reutilizado depois. Retorna 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");   // curto-circuito
    }
    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 roda: logger -> auth -> get_user
r.listen(8080);

Handlers tipados — o proc-macro @handler

@handler dá a uma fn parâmetros de extractor tipados e um retorno flexível, depois emite um wrapper fn(*HttpRequest) -> HttpResponse que o router aceita inalterado. Despacho de parâmetro, em ordem:

  • req: *HttpRequest — passagem direta, sem extração.
  • Json<T>req.body_json() + T::from_json (400 no parse, 422 no bind).
  • Path<T>Path::extract(req, "<param-name>"); o ident do parâmetro é a
  • chave do caminho.

  • Query<T>Query::extract(req, "<param-name>") contra a query string.
  • State<T>State::extract(req), lendo o slot definido por Router::state.
  • qualquer outro TT::from_request(req) via a trait FromRequest.

Forma de retorno: HttpResponse passa direto; Json<T> é auto-encapsulado via .into_response().

@handler
fn show(id: Path<i32>) -> Json<User> {
    return Json::wrap(load_user(id.val));      // .into_response() automático
}

let r: *Router = Router::new();
r.state(db as *void);
r.get("/users/:id", show);                     // o wrapper tem a forma de fn-ptr

Roteamento por atributo — @get/@post/… + routes!

Os atributos de rota (@route(METHOD, "/path") mais os açúcares @get, @post, @put, @delete, @patch, @head, @options, @any) encapsulam a fn da mesma forma que @handler faz e emitem uma função _route_register_<name>(r: *Router). routes!(r) percorre o programa e emite o registro r.<method>(...) correspondente para cada fn com atributo. Os nomes :param e *wildcard do caminho são checados em tempo de compilação contra os parâmetros Path<T> do handler (a menos que o handler receba um único req: *HttpRequest, que desativa isso).

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 se o corpo falhar no bind
}

fn main() -> i32 {
    let r: *Router = Router::new();
    defer r.free();
    routes!(r);                                // registra get_user + create_user
    return r.listen(8080).val;
}

@middleware(a, b) empilha middleware com escopo de rota que o registro gerado encadeia através de route_with, então a cadeia global use_mw é preservada e a, b rodam após ela. O atributo @listen(port) / @listen_workers(port, n) em fn main constrói o router, roda routes!, e chama listen / listen_workers para você.

Binding de requisição — FromRequest

Parâmetros de handler podem ser extractors tipados em vez de um *HttpRequest cru. Qualquer tipo que implemente FromRequest (ou, para os wrappers genéricos Json<T> / Authorization<S> / Path<T> / Query<T> / State<T>, exponha um from_request/extract inerente) pode ficar numa assinatura @handler; a macro o extrai da requisição e, em caso de falha, transforma a string de err numa resposta.

Extractors codificam o status HTTP desejado como um prefixo "<code>:<message>" na sua string de err (ex.: "401:missing token"). HttpResponse::from_extract_err faz o parse desse prefixo; um err simples (sem prefixo) usa 422 como padrão.

A trait FromRequest

pub trait FromRequest {
    fn from_request(req: *HttpRequest) -> !Self;
}

Implemente uma vez e o tipo passa a ser usável como parâmetro de handler. A string de err carrega o código de status como prefixo.

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 });
    }
}

// Chame diretamente, ou deixe o @handler fazer:
let r: !ApiKey = ApiKey::from_request(req);
if !r.ok { return HttpResponse::from_extract_err(r.err); }  // resposta 401
let key: string = r.val.key;

*HttpRequest em si implementa FromRequest como um passthrough identidade, então um handler ainda pode receber a requisição crua.

BearerAuthorization: Bearer <token>

pub struct Bearer {
    pub token: string,
}

Remove o prefixo Bearer ; erra 401 quando o header está ausente ou usa outro esquema.

@handler
fn protected(auth: Bearer) -> HttpResponse {
    return HttpResponse::ok().text("token=".concat(auth.token));
}

Headers — saco completo de headers

pub struct Headers {
    pub req: *HttpRequest,
}
  • get(self: *Headers, name: string) -> string — busca case-insensitive; "" quando ausente.
  • has(self: *Headers, name: string) -> booltrue quando o header está presente e não-vazio.
@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> — esquema de auth genérico

pub trait AuthScheme {
    fn parse(value: string) -> !Self;
    fn name() -> string;
}

pub struct Authorization<S> {
    pub scheme: S,
}

Authorization<S: AuthScheme> lê o header Authorization (ausente → 401) e entrega o valor cru (com prefixo incluso) para S::parse. O extractor estático tem o formato Authorization::extract via o método inerente:

  • from_request(req: *HttpRequest) -> !Authorization<S>

Bearer e Basic ambos implementam AuthScheme, então Authorization<Bearer> e Authorization<Basic> funcionam de imediato.

@handler
fn whoami(auth: Authorization<Bearer>) -> HttpResponse {
    return HttpResponse::ok().text(auth.scheme.token);
}

Basic — usuário + senha

pub struct Basic {
    pub user: string,
    pub pass: string,
}

Basic implementa AuthScheme. Nota: a v1 não faz decode base64 — ela trata o valor após Basic como o literal user:pass, separando no primeiro :.

let r: !Basic = Basic::parse("Basic alice:s3cret");
if r.ok {
    // r.val.user => "alice", r.val.pass => "s3cret"
}

State<T> — estado de aplicação compartilhado

pub struct State<T> {
    pub val: *T,
}

Estado registrado com Router::state(p) é alcançável sem globais. O extractor faz cast do slot para *T; um slot vazio erra 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> e Query<T> — escalares tipados

pub trait FromPath {
    fn from_path(s: string) -> !Self;
}

pub struct Path<T> {
    pub val: T,
}

pub struct Query<T> {
    pub val: T,
}

FromPath é implementado para string, i32 e bool. A macro @handler usa o ident do parâmetro como chave de busca: Path<T>req.param(name) (ausente → 404), Query<T>req.query(name) (ausente → 400); ambos convertem via T::from_path (valor inválido → 400).

  • Path::extract(req: *HttpRequest, name: string) -> !Path<T>
  • Query::extract(req: *HttpRequest, name: string) -> !Query<T>
// rota /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()));
}

Chamando um extractor manualmente:

let r: !Path<i32> = Path::extract(req, "id");
if !r.ok { return HttpResponse::from_extract_err(r.err); }  // 404 ou 400
let id: i32 = r.val.val;

Form — extractor de body urlencoded

pub struct Form {
    pub data: *FormData,
}

Extrai um body POST application/x-www-form-urlencoded. Erra 415 quando o Content-Type não bate, 400 quando o body falha no parse.

  • get(self: *Form, name: string) -> string — primeiro valor para name; "" quando ausente.
  • has(self: *Form, name: string) -> booltrue quando ao menos um campo bate.
@handler
fn submit(form: Form) -> HttpResponse {
    if !form.has("user") { return HttpResponse::with_status(400); }
    return HttpResponse::ok().text("welcome, ".concat(form.get("user")));
}

Cookies — saco de cookies da requisição

pub struct Cookies {
    pub header: string,
}

Envolve o header de requisição Cookie: (somente leitura).

  • get(self: *Cookies, name: string) -> string — valor para name; "" quando ausente.
  • has(self: *Cookies, name: string) -> booltrue quando presente.
@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));
}

MultipartFormmultipart/form-data de entrada

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 implementa FromRequest: verifica que o Content-Type é multipart/form-data, lê o boundary= e divide o body em fields simples e files enviados. Erra 400 em 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

Transforma a string de err de um extractor numa resposta. Um prefixo "<code>:<message>" define o status e usa a mensagem como body; uma string sem prefixo usa 422 como padrão.

let r: !Bearer = Bearer::from_request(req);
if !r.ok { return HttpResponse::from_extract_err(r.err); }

Binding de JSON tipado — Json<T>

Json<T> carrega um valor tipado de e para uma resposta JSON. O tipo do valor precisa implementar JsonBind (from_json / to_json).

Json<T> e json_respond

pub struct Json<T> {
    pub val: T,
}
  • Json::wrap(v: T) -> Json<T> — envolve um valor T: JsonBind.
  • json_respond<T: JsonBind>(v: T) -> HttpResponse — equivalente em uma linha a Json::wrap(v).into_response(); emite um 200 com o body JSON e 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;
}

Implementado para HttpResponse (identidade) e para Json<T> quando T: JsonBind (serializa T.to_json() num 200). Use para ergonomia na cauda do handler:

let r: HttpResponse = Json::wrap(user).into_response();

Para fazer binding de um body JSON de entrada, valide req.body_json() contra T::from_json manualmente (o typer ainda não aceita um destructure de parâmetro de fn Json<T> nem um retorno de handler Json<T>), e então responda com json_respond.

Forms URL-encoded — FormData

pub struct FormData { /* pares (name, value) opacos */ }

Uma lista ordenada de pares (name, value) para bodies application/x-www-form-urlencoded. A ordem é preservada e chaves duplicadas são mantidas.

  • FormData::new() -> *FormData — form vazio.
  • set(self: *FormData, name: string, value: string) — anexa um par.
  • len(self: *FormData) -> i32 — contagem de pares.
  • name_at(self: *FormData, i: i32) -> string / value_at(self: *FormData, i: i32) -> string — percorre por índice.
  • encode(self: *FormData) -> string — constrói a string de fio k=v&k=v percent-encoded.

Funções livres:

  • form_decode(s: string) -> !*FormData — faz parse de um body k=v&k=v; erra em percent-encoding inválido.
  • http_post_form(url: string, form: *FormData) -> !*HttpResponse — POST urlencoded de uma só vez.
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); }

Bodies multipart — Multipart

pub struct Multipart { /* boundary + parts opacos */ }

Constrói um body multipart/form-data de saída (RFC 7578).

  • Multipart::new() -> *Multipart — boundary aleatório de 24 chars.
  • Multipart::with_boundary(boundary: string) -> *Multipart — boundary fixo (testes determinísticos).
  • add_field(self: *Multipart, name: string, value: string) — campo de texto simples.
  • add_file(self: *Multipart, name: string, filename: string, content_type: string, data: string) — parte de arquivo (bytes crus).
  • content_type(self: *Multipart) -> stringmultipart/form-data; boundary=... para o header Content-Type.
  • body(self: *Multipart) -> string — serializa todas as partes para o body de fio.
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); }

O lado de recebimento é o MultipartForm (acima), parseado via sua impl FromRequest.

Cookies de cliente — 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 { /* lista de cookies opaca */ }

CookieJar é o store do lado do cliente (estilo RFC 6265). Anexe a um HttpClient para que respostas Set-Cookie sejam absorvidas e requisições de saída carreguem Cookie: automaticamente.

Cookie:

  • Cookie::new(name: string, value: string) -> *Cookie — escopo padrão (path="/", sem restrição de domínio, não secure, não http-only). Ajuste os campos diretamente.

CookieJar:

  • CookieJar::new() -> *CookieJar — jar vazio.
  • ingest(self: *CookieJar, request_host: string, set_cookie_lines: *Vector<string>) — faz parse e armazena toda linha Set-Cookie, escopada a request_host; mesmo (name, domain, path) substitui.
  • header_value(self: *CookieJar, host: string, path: string, is_secure: bool) -> string — constrói o valor Cookie: de saída; "" quando nenhum se aplica.
  • all(self: *CookieJar) -> *Vector<*Cookie> — snapshot de todo cookie.
  • clear(self: *CookieJar) — esquece todo cookie.
import stdlib::http::client::*;
import stdlib::http::cookies::*;

let jar: *CookieJar = CookieJar::new();
let c: *HttpClient = HttpClient::new();
c.jar = jar;

// Login: o Set-Cookie do servidor é absorvido.
let _ = c.post_json("https://api.example.com/login", body);

// Requisições subsequentes carregam Cookie: automaticamente.
let r: !*HttpResponse = c.get("https://api.example.com/me");

// Inspecione o jar diretamente:
let h: string = jar.header_value("api.example.com", "/me", true);
// h pode ser "session=abc123"
jar.clear();   // logout

Recursos HTTP — CORS, SSE, estáticos, compressão, proxy, JWT

Um conjunto de middlewares e helpers HTTP plug-and-play construídos sobre o pipeline Router / Chain e os tipos HttpRequest / HttpResponse. Cada um vive em seu próprio submódulo stdlib::http::* e é pub-importado individualmente:

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 compartilham a assinatura fn(req: *HttpRequest, c: *Chain) -> HttpResponse e são registrados com r.use_mw(...). Cada um chama chain_next(c) para invocar o resto do pipeline e então pós-processa o resultado. Helpers que tocam o sistema de arquivos ou a rede retornam !T e expõem falhas através de .ok / .err.

CORS — CorsConfig

CorsConfig guarda os valores emitidos como cabeçalhos de resposta Access-Control-Allow-*. install_cors os armazena num slot global do processo; cors_mw é o middleware que os carimba em cada resposta (e curto-circuita requisições preflight OPTIONS com um 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,     // emite Allow-Credentials: true quando definido
    pub max_age:     i32,      // segundos de Access-Control-Max-Age (0 = omitir)
}
Símbolo Assinatura Descrição
CorsConfig::permissive pub fn permissive() -> CorsConfig Origem *, todos os métodos/cabeçalhos comuns, sem credenciais, sem max-age.
install_cors pub fn install_cors(cfg: CorsConfig) Armazena a config no slot CORS global do processo lido por cors_mw.
cors_mw pub fn cors_mw(req: *HttpRequest, c: *Chain) -> HttpResponse Middleware: responde OPTIONS com 204, caso contrário adiciona os cabeçalhos CORS a 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 + o bloco de cabeçalhos CORS
    return r.listen_workers(8080, 4).val;
}

Para uma política restrita, monte a config à mão:

install_cors(CorsConfig {
    origin:      "https://app.example.com",
    methods:     "GET, POST",
    headers:     "Content-Type, Authorization",
    credentials: true,
    max_age:     600,
});
// respostas carregam Allow-Credentials: true e Max-Age: 600

Server-Sent Events — SseEvent

SSE transmite frames text/event-stream sobre chunked transfer. Um handler retorna um HttpResponse cujo corpo é um *dyn ChunkSource; o servidor puxa um evento por vez e o enquadra. SseEvent é o modelo de fio.

pub struct SseEvent {
    pub event: string,   // event: nome    ("" = sem nome)
    pub id:    string,   // id: valor      ("" = omitir)
    pub data:  string,   // data: payload  (multi-linha dividido conforme spec)
    pub retry: i32,      // retry: ms      (0 = omitir)
}
Símbolo Assinatura Descrição
SseEvent::data pub fn data(payload: string) -> SseEvent Evento só de dados, sem nome/id/retry.
SseEvent::named pub fn named(name: string, payload: string) -> SseEvent Evento nomeado (evtSource.addEventListener(name, ...)).
sse_format pub fn sse_format(e: SseEvent) -> string Serializa um evento em bytes de fio; um evento vazio é uma única linha em branco.
sse_comment pub fn sse_comment(text: string) -> string Uma linha de comentário : text\n\n (heartbeat).
sse_keepalive pub fn sse_keepalive() -> string Frame de heartbeat mínimo :\n\n.
sse_last_event_id pub fn sse_last_event_id(req: *HttpRequest) -> string O cabeçalho Last-Event-ID do cliente ("" quando ausente).
sse_response pub fn sse_response(source: *dyn ChunkSource) -> HttpResponse Envolve um ChunkSource com o content-type SSE + cabeçalhos no-buffer.
SSE_END pub const SSE_END: string Nome de evento sentinela que fecha um stream SseChannel ("__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;
}

Um stream dirigido por produtor implementa ChunkSource::next_chunk e retorna some(sse_format(...)) por evento, none() no fim:

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);   // transmite data: 1..5
}

SseChannel distribui um chan<SseEvent> de broadcast para um cliente. Cada valor enviado ao chan vira um frame; enviar um evento cujo .event seja igual a SSE_END encerra o stream.

pub struct SseChannel { pub ch: *chan<SseEvent> }
Símbolo Assinatura Descrição
SseChannel::new pub fn new(ch: *chan<SseEvent>) -> *SseChannel Envolve um chan como um 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);
}
// em outro lugar: events.send(SseEvent::named("ping", "1"));
// para fechar:    events.send(SseEvent { event: SSE_END, id: "", data: "", retry: 0 });

Arquivos estáticos — StaticOpts

serve_dir monta um diretório num prefixo de URL e serve arquivos através de HttpResponse::file (sendfile zero-copy). Ele adiciona proteção contra path-traversal (rejeita .., / inicial, bytes NUL), uma ETag fraca a partir do tamanho do arquivo com suporte a If-None-Match304, um Cache-Control opcional e index.html para caminhos de diretório.

pub struct StaticOpts {
    pub root:          string,   // diretório no disco
    pub url_prefix:    string,   // segmento de URL sob o qual os arquivos montam
    pub cache_control: string,   // cabeçalho Cache-Control ("" = omitir)
}
Símbolo Assinatura Descrição
serve_dir pub fn serve_dir(r: *Router, opts: StaticOpts) Registra GET <url_prefix>/*filepath servindo <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;
}

Compressão — gzip_mw

gzip_mw comprime o corpo da resposta com gzip (zlib, linkado -lz) após rodar o resto da chain. Ele só atua quando a requisição anunciou Accept-Encoding: gzip, o status é 2xx, nenhum Content-Encoding já está definido, o corpo é ≥ 1024 bytes e o Content-Type não é um tipo de mídia já comprimido (image/video/audio, zip, gzip, octet-stream). Quando comprime, ele substitui o corpo e define Content-Encoding: gzip.

Símbolo Assinatura Descrição
gzip_mw pub fn gzip_mw(req: *HttpRequest, c: *Chain) -> HttpResponse Aplica gzip na resposta downstream quando as condições acima valem.
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);
    // requisição com Accept-Encoding: gzip -> corpo com gzip, Content-Encoding: gzip
    // requisições pequenas / sem aceitar gzip -> corpo passa intacto
    return r.listen_workers(8080, 4).val;
}

Reverse proxy — ReverseProxyConfig

reverse_proxy encaminha uma requisição de entrada para uma origem upstream e rebaixa a resposta upstream de volta para o HttpResponse do servidor. Ele remove cabeçalhos hop-by-hop, carimba X-Forwarded-*, opcionalmente remove um prefixo de caminho e transforma erros de rede/parsing em 502 Bad Gateway.

pub struct ReverseProxyConfig {
    pub upstream:         string,   // origem, ex. "http://127.0.0.1:9000"
    pub strip_prefix:     string,   // remove do caminho antes de encaminhar
    pub preserve_host:    bool,     // mantém o cabeçalho Host do cliente no upstream
    pub rewrite_location: bool,     // reescreve Location do upstream para public_base
    pub public_base:      string,   // base usada por rewrite_location
    pub timeout_ms:       i32,      // timeout da requisição upstream
    pub tls_insecure:     bool,     // pula verificação TLS do upstream
}
Símbolo Assinatura Descrição
ReverseProxyConfig::to pub fn to(upstream: string) -> ReverseProxyConfig Config com defaults conservadores (timeout de 30s, sem strip de prefixo, verifica TLS).
reverse_proxy pub fn reverse_proxy(cfg: *ReverseProxyConfig, req: *HttpRequest) -> HttpResponse Encaminha req upstream; 502 em caso de falha.
proxy_build_upstream_request pub fn proxy_build_upstream_request(cfg: *ReverseProxyConfig, req: *HttpRequest) -> *HttpClientRequest Constrói (mas não envia) a requisição de cliente upstream.
proxy_target_url pub fn proxy_target_url(cfg: *ReverseProxyConfig, path: string) -> string Calcula a URL upstream para um caminho de requisição (aplica strip_prefix).
proxy_filter_request_headers pub fn proxy_filter_request_headers(block: string, preserve_host: bool) -> string Descarta hop-by-hop (e Host a menos que preservado) de um bloco de cabeçalhos de requisição.
proxy_filter_response_headers pub fn proxy_filter_response_headers(block: string) -> string Descarta cabeçalhos hop-by-hop / de framing de um bloco de resposta upstream.
proxy_rewrite_location pub fn proxy_rewrite_location(loc: string, upstream: string, public_base: string) -> string Reescreve um Location upstream absoluto para public_base; os relativos passam intactos.
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, resposta repassada de volta
    return 0;
}

Monte um upstream sob um prefixo local e reescreva redirecionamentos para o host público:

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;
}

Verificação JWT — JwtClaims

jwt_verify checa um JWT HS256 compacto contra um segredo compartilhado: valida a estrutura de três partes, recalcula a assinatura HMAC-SHA-256 sobre header.payload, exige uma claim sub string e rejeita tokens expirados (exp no passado). Em sucesso, retorna as claims decodificadas; erros carregam um prefixo de status HTTP ("400:...", "401:...").

pub struct JwtClaims {
    pub sub: string,        // claim de subject (obrigatória)
    pub exp: i32,           // segundos epoch de expiração (0 = sem claim exp)
    pub iat: i32,           // segundos epoch de issued-at (0 = ausente)
    pub raw: *JsonValue,    // objeto de payload decodificado completo
}
Símbolo Assinatura Descrição
jwt_verify pub fn jwt_verify(token: string, key: string) -> !JwtClaims Verifica um token HS256; err em malformado/assinatura-ruim/expirado.
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);                    // ex. "user-42"
    println!(claims.exp.to_string());        // ex. "1900000000"
    return 0;
}

Usado dentro de um handler, o erro com prefixo de status mapeia de forma limpa para uma resposta:

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

Dois transportes mais recentes ficam por cima das camadas TLS e UDP vistas antes neste capítulo. HTTP/2 (stdlib::http::h2) é Glide puro: framing, uma máquina de estados de conexão e compressão de headers HPACK, com tanto um cliente (H2Conn) quanto um loop de servidor (h2_serve / h2_listen). HTTP/3 (stdlib::http::h3) embrulha QUIC via libngtcp2 + libnghttp3 e expõe um pequeno conjunto de funções livres que espelham o formato do cliente HTTP/1.1.

Ambos negociam o protocolo através do TLS ALPN. O cliente opta por "h2,http/1.1" (ou "h3") para que um servidor que não fale a nova versão faça fallback de forma transparente. A v1 de cada transporte é orientada a GET-e-POST-simples: sem PUSH, sem obedecer a priority, sem trailers.

Ressalva de plataforma para HTTP/3

HTTP/3 linka -lngtcp2 -lngtcp2_crypto_ossl -lnghttp3 -lssl -lcrypto e precisa da API QUIC do OpenSSL 3.5+ mais o shim estático ngtcp2_crypto_ossl. Essa combinação está presente no sysroot windows-gnu que vem com o release, mas não nos sysroots musl (Linux) ou macOS hoje. Na prática, HTTP/3 é exclusivo de Windows nos sysroots distribuídos — no Linux/macOS o linker faz fallback para as libs do sistema, que só resolvem se você instalou um toolchain OpenSSL 3.5+ / ngtcp2 por conta própria. HTTP/2 não tem tal dependência; funciona em todos os targets.


FrameType e FrameFlag — opcodes do HTTP/2

Frames HTTP/2 carregam um comprimento de 24 bits, um tipo de 8 bits, flags de 8 bits e um stream id de 31 bits (RFC 7540 §4.1). FrameType e FrameFlag são structs marcadoras vazias cujos métodos estáticos retornam as constantes da spec — use-os em vez de inteiros literais.

Métodos de FrameType (cada um retorna 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

Métodos de FrameFlag (cada um retorna i32, combine com |):

FrameFlag::end_stream()   // 1
FrameFlag::end_headers()  // 4
FrameFlag::padded()       // 8
FrameFlag::priority()     // 32
FrameFlag::ack()          // 1  (compartilha o bit 1 com end_stream; nunca no mesmo frame)

FrameHeader — um header de frame decodificado

pub struct FrameHeader {
    pub length:     i32,   // comprimento do payload, 24 bits
    pub frame_type: i32,   // tipo, 8 bits
    pub flags:      i32,   // flags, 8 bits
    pub stream_id:  i32,   // stream id, 31 bits (bit do topo reservado)
}

Funções de encode / decode de frame

  • write_frame(buf: *ByteBuffer, frame_type: i32, flags: i32, stream_id: i32, payload: string) — anexa um header de 9 bytes + o payload string. Trunca no primeiro NUL (o string do Glide é uma cstring), então use-o apenas para payloads de texto.
  • write_frame_bytes(buf: *ByteBuffer, frame_type: i32, flags: i32, stream_id: i32, payload: *ByteBuffer) — o mesmo, mas o payload vem de um ByteBuffer com comprimento explícito. Seguro para binário; use isto para blocos de header HPACK.
  • read_frame_header(raw: *ByteBuffer, pos: i32) -> !*FrameHeader — faz parse de um header de 9 bytes em pos.
  • h2_preface() -> string — o preâmbulo de conexão de 24 bytes "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".
  • settings_payload_empty() -> string — um payload SETTINGS vazio (usado para dar ACK nos settings do peer).
  • settings_pair_to(buf: *ByteBuffer, key: i32, value: i32) — anexa uma entrada SETTINGS de 6 bytes.
  • window_update_to(buf: *ByteBuffer, increment: i32) — anexa um payload WINDOW_UPDATE de 4 bytes.
import stdlib::bytes::*;
import stdlib::http::h2::*;

let buf: *ByteBuffer = ByteBuffer::new();
defer buf.free();
write_frame(buf, FrameType::ping(), 0, 0, "ABCDEFGH");   // PING de 8 bytes

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 — compressão de headers HPACK

HPACK (RFC 7541) comprime campos de header HTTP/2 contra uma tabela estática de 61 entradas mais uma tabela dinâmica por direção. O encoder nunca emite literais Huffman (legal pela spec, H=0 sempre); o decoder suporta Huffman por completo porque servidores reais o emitem. Reutilize um encoder e um decoder durante toda a vida da conexão para que a tabela dinâmica se acumule.

pub struct HpackEncoder { pub max_table_size: i32, /* tabelas internas */ }
pub struct HpackDecoder { pub max_table_size: i32, /* tabelas internas */ }

/// Um campo de header decodificado, na ordem do fio.
pub struct HpackHeader {
    pub name:  string,
    pub value: string,
}

HpackEncoder:

  • HpackEncoder::new() -> *HpackEncoder — cap de 4 KiB na tabela dinâmica.
  • encode_header(self, buf: *ByteBuffer, name: string, value: string) — anexa um header a buf.

HpackDecoder:

  • HpackDecoder::new() -> *HpackDecoder — cap de 4 KiB na tabela dinâmica.
  • decode_block(self, raw: string, len: i32) -> !*Vector<*HpackHeader> — decodifica um bloco de header completo. len é explícito porque blocos HPACK frequentemente contêm bytes NUL.
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 pelo 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 — conexão cliente HTTP/2

H2Conn embrulha um TlsStream já com handshake feito que negociou ALPN h2. Ele envia o preâmbulo de conexão e um SETTINGS + WINDOW_UPDATE inicial na construção, depois request abre um stream, envia headers (+ body opcional) e remonta a resposta.

pub struct H2Conn {
    pub next_stream_id: i32,   // próximo stream id ímpar do cliente
    /* tls, codecs HPACK e rx ring são internos */
}

Métodos:

  • H2Conn::over(tls: *TlsStream) -> !*H2Conn — adota um stream TLS h2 com handshake feito; envia preâmbulo + SETTINGS (ENABLE_PUSH=0, INITIAL_WINDOW_SIZE=1 MiB) + um WINDOW_UPDATE de conexão.
  • request(self, method: string, path: string, authority: string, scheme: string, extra_headers: *Vector<*HpackHeader>, body: string) -> !*H2Response — envia uma requisição, lê a resposta completa.
  • close(self) — envia GOAWAY e fecha o stream TLS subjacente.

A resposta:

pub struct H2Response {
    pub status:  i32,
    pub headers: *Vector<*HpackHeader>,
    pub body:    string,
}

Uma requisição cliente completa. Note a negociação ALPN: só prossiga com H2Conn::over se o servidor realmente selecionou 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") {
        // Servidor fez fallback para HTTP/1.1 — conduza pelo cliente 1.1.
        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();   // sem headers extras
    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;
}

Para adicionar headers de requisição, empurre entradas HpackHeader no vetor extra_headers. A conexão permanece aberta após request retornar, então uma segunda chamada reutiliza a mesma tabela dinâmica HPACK:

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, "");

A maioria dos usuários chega ao HTTP/2 pelo cliente de alto nível em vez disso: defina c.http2 = true em um HttpClient e ele negocia h2 via ALPN para URLs https://, fazendo fallback para HTTP/1.1 quando o servidor recusa.

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 — servidor HTTP/2

O lado servidor conduz toda a vida de uma conexão cliente: valida o preâmbulo, troca SETTINGS, depois despacha cada requisição totalmente recebida para um handler. O handler roda de forma síncrona dentro do loop de leitura; streams concorrentes em uma conexão são serializados na etapa do handler.

/// Estado por stream que o servidor rastreia enquanto uma requisição chega.
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) — roda o loop do servidor HTTP/2 em um stream TLS h2 com handshake feito. Fecha o stream quando a conexão termina; o chamador não chama s.close().

O handler é o mesmo formato fn(*HttpRequest) -> HttpResponse usado em todo o restante deste capítulo, então um handler escrito para HTTP/1.1 funciona sem alterações:

import stdlib::net::tls::*;
import stdlib::http::*;
import stdlib::http::h2::*;

fn handle(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().body("served over h2");
}

// Dentro do seu próprio loop de accept TLS, uma vez que o ALPN negociou `h2`:
fn serve_conn(client: *TlsStream) {
    h2_serve(client, handle);   // assume a posse; fecha quando termina
}

HTTP/3 sobre QUIC

HTTP/3 é exposto como funções livres mais um tipo de cache de sessão. Lembre-se da ressalva de sysroot exclusivo de Windows acima. URLs devem ser https:// (ou h3://); a porta padrão é 443.

Requisições de cliente

  • http3_get(url: string, tls_insecure: bool) -> !*HttpResponse — GET de tiro único.
  • http3_request(method: string, url: string, extra_headers: string, body: string, tls_insecure: bool) -> !*HttpResponse — requisição genérica de tiro único. extra_headers é um bloco Name: Value unido por CRLF.

tls_insecure = true desabilita a verificação de certificado (apenas dev local com self-signed). Ambos retornam o 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

O parser de URL usado internamente também é público:

pub struct ParsedH3Url {
    pub host: string,
    pub port: i32,
    pub path: string,
}

H3SessionCache — retomada de sessão 0-RTT

Passar um H3SessionCache através de chamadas repetidas de http3_request_cached permite que a segunda conexão em diante para o mesmo (host, port) retome a sessão TLS e despache a requisição como early data 0-RTT (economiza ~1 RTT). Em memória por padrão; save/load o persistem em um arquivo binário entre reinicializações.

pub struct H3SessionCache { /* handle opaco */ }

Métodos:

  • H3SessionCache::new() -> *H3SessionCache
  • free(self) — libera o cache.
  • attempts(self) -> i32 — conexões que encontraram e instalaram uma sessão em cache (tentaram 0-RTT).
  • accepted(self) -> i32 — conexões cujos dados 0-RTT o servidor aceitou. attempts - accepted é a contagem de rejeições.
  • save(self, path: string) -> bool — serializa para um arquivo binário.
  • load(self, path: string) -> i32 — relê as entradas (descarta as expiradas; retorna a contagem carregada, 0 num arquivo ausente/corrompido).

E o ponto de entrada de requisição com cache:

  • http3_request_cached(method: string, url: string, extra_headers: string, body: string, tls_insecure: bool, cache: *H3SessionCache) -> !*HttpResponse — mesmo formato de http3_request mas passando o cache. Thread-safe (mutex interno).
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 na primeira execução; seguro antes de save rodar

    let r1: !*HttpResponse = http3_request_cached("GET", url, "", "", false, c);
    if !r1.ok { return 1; }       // handshake completo; ticket em cache
    let r2: !*HttpResponse = http3_request_cached("GET", url, "", "", false, c);
    if !r2.ok { return 2; }       // retoma a sessão anterior, pode usar 0-RTT

    println!("0-RTT attempts:", c.attempts(), "accepted:", c.accepted());
    c.save("h3-sessions.bin");
    return 0;
}

Servidor HTTP/3

  • h3_make_self_signed_cert(cert_path: string, key_path: string, cn: string, days: i32) -> !i32 — escreve um cert + chave PEM self-signed ECDSA-P256 (SAN cobre DNS:localhost e IP:127.0.0.1), válido por days dias. Útil para dev local / testes de loopback.
  • http3_listen(port: i32, cert_path: string, key_path: string, handler: fn(*HttpRequest) -> HttpResponse) -> !i32 — roda um servidor HTTP/3 na port UDP. Uma única thread leitora em C faz demux dos pacotes de entrada entre até 64 conexões; a requisição de cada conexão aceita é despachada para handler em sua própria pthread.

cert_path / key_path são arquivos PEM (mesmo layout de https_listen). A assinatura do handler é idêntica à dos servidores HTTP/1.1 e HTTP/2.

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 em bind / cert ruim
    return 0;
}

Limites do servidor v1: um stream de requisição por conexão, sem HTTP/3 PUSH, sem descoberta de path-MTU (assume um default seguro de 1200 bytes). Early data 0-RTT é aceita no servidor mas apenas para métodos idempotentes — requisições não-GET/HEAD chegando como 0-RTT são rejeitadas com 425 Too Early para que o cliente retente sobre 1-RTT.

Codificação de URL — url_encode

Percent-encoding e os structs de URL parseada que os clientes HTTP e WebSocket constroem internamente. O codec vive em stdlib::url; os structs parseados acompanham seus respectivos clientes (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 aplica percent-encoding em s conforme a RFC 3986 §2.3. O conjunto unreserved — letras ASCII, dígitos e - _ . ~ — passa intacto; todo outro byte vira %XX com hex em maiúsculas. UTF-8 multibyte é codificado byte a byte.

url_encode("hello world");   // => "hello%20world"
url_encode("a&b=c");         // => "a%26b%3Dc"
url_encode("test_~-.");      // => "test_~-."   (tudo unreserved)
url_encode("héllo");         // => "h%C3%A9llo" (bytes UTF-8)

url_decode é o inverso. Percorre a string, expandindo cada %XX de volta para um byte e copiando todo o resto na íntegra. Note que + não é tratado como espaço — ele é preservado literalmente, casando com o que url_encode produz (um espaço vira %20, nunca +). Um % no fim da string ou seguido por um dígito não-hex é um erro fatal, então a função retorna !string: leia .ok, depois .val ou .err.

let r: !string = url_decode("hello%20world");
if r.ok { println!(r.val); }       // "hello world"

url_decode("%c3%a9").val;          // => "é"  (hex em minúsculas aceito)
url_decode("a+b").val;             // => "a+b" (plus mantido 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); }  // malformado (escape truncado)

O round-trip encode/decode é sem perdas sobre entrada arbitrária:

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
}

Construindo e parseando query strings

Não há um construtor dedicado de query-map em stdlib::url — uma query string é apenas um componente comum de URL, então você compõe uma codificando cada chave e valor com url_encode e juntando você mesmo:

let q: string = "q=".concat(url_encode("hello world"))
    .concat("&lang=").concat(url_encode("en"));
// q => "q=hello%20world&lang=en"

No lado do servidor, a query string é parseada pra você: um http::HttpRequest expõe .query(name) (coberto na seção do servidor HTTP), então código de aplicação raramente decodifica parâmetros de query à mão.

ParsedUrl — URLs HTTP

O cliente HTTP parseia uma URL http:// ou https:// em um ParsedUrl. O struct é público pra você poder ler os componentes pra onde uma requisição foi roteada; o parser em si é interno (construído por HttpClient), então você normalmente obtém um indiretamente em vez de construí-lo.

pub struct ParsedUrl {
    pub scheme: string,   // "http" ou "https"
    pub host:   string,   // host puro (literais IPv6 desembrulhados de [..])
    pub port:   i32,      // porta explícita, senão 80 (http) / 443 (https)
    pub path:   string,   // path + query, nunca vazio ("/" quando ausente)
}

port assume o padrão a partir do scheme (80 para http, 443 para https) e é sobrescrito por um :port explícito. Hosts IPv6 são aceitos em forma de colchetes (https://[::1]:8443/) e os colchetes são removidos de host.

host_header

pub fn host_header(self: *ParsedUrl) -> string

Formata o valor para um header HTTP Host:. A porta é omitida quando é igual ao padrão do scheme, e incluída caso contrário.

// https://example.com/        -> host_header() == "example.com"
// https://example.com:8443/x  -> host_header() == "example.com:8443"

WsParsedUrl — URLs WebSocket

O cliente WebSocket usa um struct paralelo para URLs ws:// / wss://. Ele carrega uma flag booleana is_wss em vez de uma string de scheme, mas no restante tem a mesma forma de ParsedUrl.

pub struct WsParsedUrl {
    pub is_wss: bool,     // false = ws, true = wss
    pub host:   string,
    pub port:   i32,      // porta explícita, senão 80 (ws) / 443 (wss)
    pub path:   string,   // nunca vazio ("/" quando ausente)
}

host_header

pub fn host_header(self: *WsParsedUrl) -> string

Mesmo contrato de ParsedUrl::host_header: descarta a porta quando ela casa com o padrão do scheme (80 para ws, 443 para wss), mantém caso contrário.

// wss://example.com/         -> host_header() == "example.com"
// wss://example.com:8443/    -> host_header() == "example.com:8443"

ParsedUrl e WsParsedUrl têm deliberadamente os mesmos quatro campos (host / port / path mais um discriminador de scheme) pra que a lógica de conexão em ambos os clientes seja idêntica: resolver host, discar port, enviar path e emitir host_header() como o header Host:. A única diferença é quais portas padrão e nomes de scheme se aplicam.