Capítulo 31 97 min de leitura

Construindo serviços HTTP

O Glide inclui um servidor HTTP/1.1 em stdlib::http, construído sobre o stdlib::net assíncrono do capítulo de concorrência. Toda leitura e escrita estaciona a corrotina em vez de bloquear uma thread, então um único processo mantém dezenas de milhares de conexões ativas. Este capítulo parte do menor servidor possível até uma API JSON completa, camada por camada.

Olá, servidor

O menor servidor possível é uma função handler mais http_listen:

hello_server.glide
import stdlib::http::*;

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

fn main() -> !i32 {
    http_listen(8080, root)?;
    return ok(0);
}

Um handler é fn(*HttpRequest) -> HttpResponse — ele recebe a requisição por ponteiro e retorna uma resposta por valor. http_listen(port, handler) vincula a porta e serve indefinidamente; ele retorna !i32, então propagamos o erro de bind com ? (note que main retorna !i32 para permitir isso).

Execute-o e, em outro terminal:

shell
curl http://127.0.0.1:8080/anything
# → hello from /anything

Esse é o formato completo de um servidor Glide. O restante deste capítulo expande cada parte: os pontos de entrada que vinculam a porta, os tipos de requisição e resposta, e as camadas de roteamento, middleware e extractor que ficam por cima.

Servindo: http_listen, HTTPS e workers

Todo servidor HTTP em Glide se resume a duas coisas: um handler — uma função simples que transforma uma requisição em uma resposta — e uma chamada listen que vincula uma porta e encaminha as requisições para esse handler. Não há objeto de framework para construir nessa camada; roteamento, middleware e extractors vivem em cima desses pontos de entrada. Acerte os pontos de entrada e todo o resto se encaixa.

O handler

Um handler é simplesmente uma função com exatamente esse formato:

fn(*HttpRequest) -> HttpResponse

Ele recebe um ponteiro para a requisição totalmente armazenada em buffer e retorna uma resposta por valor. O tempo de vida da requisição está vinculado à chamada — o próprio comentário de documentação de HttpRequest avisa: "não armazene o ponteiro." Você passa o handler pelo nomeroot, não root(). É um valor de função que corresponde a fn(*HttpRequest) -> HttpResponse.

http_listen — o ponto de entrada simples

pub fn http_listen(port: i32, handler: fn(*HttpRequest) -> HttpResponse) -> !i32

http_listen vincula port e fica em loop aceitando conexões para sempre. Ele retorna !i32, então propagamos a falha de bind com ? (retorna err("bind failed") se a porta não puder ser vinculada). Na prática o ok(0) no final nunca é alcançado — o loop de accept roda até o processo ser encerrado — mas a assinatura permite falhar rapidamente em um bind inválido.

O modelo de concorrência é corrotina por conexão (M:N): quando a plataforma tem um reator assíncrono configurado (epoll no Linux, kqueue no macOS/BSD), cada conexão aceita é tratada por uma corrotina com spawn. Toda leitura e escrita de socket estaciona essa corrotina em vez de fixar uma thread do SO, então um único processo pode manter dezenas de milhares de conexões concorrentes com baixo custo. Em plataformas sem reator, http_listen volta a tratar cada conexão serialmente na thread chamadora — correto, apenas não concorrente.

https_listen — TLS com ALPN

pub fn https_listen(port: i32, cert_path: string, key_path: string,
                    handler: fn(*HttpRequest) -> HttpResponse) -> !i32

Mesmo formato de handler, mais os caminhos para o certificado PEM e a chave privada. O certificado e a chave são passados como caminhos do sistema de arquivos (strings), não como blobs em memória — o servidor os lê ao vincular a porta.

Internamente, https_listen anuncia tanto HTTP/2 quanto HTTP/1.1 via ALPN ("h2,http/1.1"). Clientes que suportam h2 negociam o protocolo binário; clientes que só entendem HTTP/1.1 fazem fallback de forma transparente. Seu handler não muda em nenhum dos casos. Requisições que chegaram via TLS têm req.tls == true, que o middleware de proxy reverso usa para estampar o X-Forwarded-Proto correto.

import stdlib::http::*;

fn root(req: *HttpRequest) -> HttpResponse {
    if req.path.eq("/health") {
        return HttpResponse::ok().body("ok");
    }
    return HttpResponse::ok()
        .set("Content-Type", "text/plain")
        .body("secure: ".concat(req.path));
}

fn main() -> !i32 {
    https_listen(8443, "certs/server.crt", "certs/server.key", root)?;
    return ok(0);
}

Variantes com workers — quando um único loop de accept não é suficiente

http_listen executa um único loop de accept. Em uma máquina com múltiplos núcleos, você pode distribuir os accepts entre várias threads worker. Há dois pontos de entrada com workers, e a diferença entre eles é a escolha mais importante que você faz aqui.

pub fn http_listen_workers(port: i32, n: i32,
                           handler: fn(*HttpRequest) -> HttpResponse) -> !i32

pub fn http_listen_workers_blocking(port: i32, n: i32,
                                    handler: fn(*HttpRequest) -> HttpResponse) -> !i32
Função Quando usar Restrições do handler Throughput (referência: box de 4 núcleos)
http_listen_workers Endpoints sem estado e de alto tráfego (health checks, cache hits, JSON de CPU pura) O caminho rápido não estaciona — mantenha o handler sem bloqueios ~159k req/s
http_listen_workers_blocking Handlers que genuinamente bloqueiam: consultas ao DB, HTTP downstream, sincronização via chan Nenhuma — bloquear estaciona a corrotina da conexão, não a thread ~107k req/s

http_listen_workers tenta primeiro um caminho de despacho rápido via máquina de estados (Linux); se o SO não suportar (-1 de retorno), faz fallback transparente para http_listen_workers_blocking. Portanto, quando houver dúvida sobre se você bloqueia, use a variante blocking — ela é mais lenta, mas sem restrições, e é o padrão seguro para qualquer handler que acesse um banco de dados ou outro serviço.

Um pool de workers sem estado:

import stdlib::http::*;

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

fn main() -> !i32 {
    // N worker threads; the kernel load-balances accepts across them.
    http_listen_workers(8080, 4, handler)?;
    return ok(0);
}

E o pool com capacidade de bloqueio, para handlers que fazem I/O real:

import stdlib::http::*;

fn handler(req: *HttpRequest) -> HttpResponse {
    // May block: DB queries, downstream HTTP, chan recv.
    // Each connection parks its own corrotina, not the worker thread.
    return HttpResponse::ok().body("blocking-capable: ".concat(req.method));
}

fn main() -> !i32 {
    http_listen_workers_blocking(8080, 4, handler)?;
    return ok(0);
}

Escolhendo um ponto de entrada

  • Um núcleo, serviço simples ou apenas começando → `http_listen`.
  • TLS / HTTPS (e HTTP/2 gratuito) → `https_listen`.
  • Multi-núcleo, handlers que acessam um DB ou chamam outros serviços → `http_listen_workers_blocking`.
  • Multi-núcleo, endpoints sem estado e de alto tráfego onde você confirmou que o handler nunca estaciona → `http_listen_workers`.

Todos os quatro compartilham a mesma assinatura de handler, então você pode trocar os pontos de entrada sem alterar seu handler. E todos os quatro retornam !i32 — propague o erro de bind com ? e você terá uma falha limpa se a porta estiver ocupada.

Requisições e respostas

Cada ponto de entrada acima passa ao seu handler os mesmos dois tipos. HttpRequest é a visão totalmente armazenada em buffer e somente leitura do que chegou pela rede; HttpResponse é um pequeno valor imutável que você monta com uma cadeia de builder. Ambos vivem em stdlib::http.

HttpRequest: os campos

A requisição é uma struct simples. Seus campos são todos pub, então você pode lê-los diretamente — não há cerimônia de getters para a linha de requisição:

pub struct HttpRequest {
    pub method:        string,            // "GET", "POST", ...
    pub path:          string,            // "/users/42?fields=name" (raw, with query)
    pub version:       string,            // "HTTP/1.1"
    pub headers_block: string,            // raw "Name: Value\r\n..." text
    pub body:          string,            // fully buffered request body
    pub params:        *HashMap<string>,  // router path params (may be null)
    pub queries:       *HashMap<string>,  // parsed query map (lazy; may be null)
    pub tls:           bool,              // true when served via https_listen
}

Algumas coisas que vale internalizar:

  • `path` é o alvo bruto, incluindo a query string. Use path_only() quando quiser apenas a parte da rota, e query(name) para extrair um valor de query decodificado — não faça parse manual de path.
  • `headers_block` é o texto bruto do protocolo. Você pode lê-lo, mas prefira header(name); ele faz uma busca sem distinção de maiúsculas/minúsculas e armazena em cache o resultado.
  • `tls` é como o middleware sabe se a requisição chegou criptografada (é usado para, por exemplo, estampar X-Forwarded-Proto). É true apenas sob https_listen.

HttpRequest: os métodos

A impl fornece acessores ergonômicos. Todos eles recebem self: *HttpRequest e nunca alocam por surpresa — os mapas de header e query são construídos uma vez e armazenados em cache.

Método Retorna O que faz
header(name) string Busca de header sem distinção de maiúsculas/minúsculas; "" se ausente. A primeira chamada faz o parse de headers_block em um cache; repetições são O(1).
param(name) string Parâmetro de rota do router (ex: :id); "" se nenhum router correspondeu ou o nome é desconhecido.
query(name) string Valor da query string decodificado; "" se ausente. Faz o parse de ?k=v&… de path no primeiro uso e armazena em cache.
path_only() string path com o segmento de query ?… removido.
is_method(m) bool Verificação de método sem distinção de maiúsculas/minúsculas, ex: req.is_method("GET").
body_json() !*JsonValue Faz o parse do corpo como JSON; err em entrada vazia ou malformada.

Ler de uma requisição se parece com isso — cada acessor retorna "" para o caso ausente, então não há ?T para desempacotar no lado da busca:

import stdlib::http::*;

fn handler(req: *HttpRequest) -> HttpResponse {
    let host:  string = req.header("Host");       // "" if no Host header
    let id:    string = req.param("id");          // "" without a router match
    let page:  string = req.query("page");        // "" if no ?page=
    let route: string = req.path_only();          // "/users/42" from "/users/42?x=1"

    if req.is_method("GET") {
        return HttpResponse::ok().text(host.concat(id).concat(page).concat(route));
    }
    return HttpResponse::ok().text("hi");
}

Lendo um corpo JSON

body_json() retorna !*JsonValue — um Result, porque o corpo pode estar vazio ou não ser um JSON válido. A assinatura é:

pub fn body_json(self: *HttpRequest) -> !*JsonValue

Verifique .ok (ou propague com ?) antes de tocar no valor. Os getters tipados de JsonValue, como get_string, por si mesmos retornam um Result, então um bind completo é feito em alguns passos com guardas:

import stdlib::http::*;
import stdlib::json::*;

fn create(req: *HttpRequest) -> HttpResponse {
    let v: !*JsonValue = req.body_json();
    if !v.ok {
        return HttpResponse::with_status(400).text(v.err);   // empty / invalid JSON
    }
    let name: !string = v.val.get_string("name");
    if !name.ok {
        return HttpResponse::with_status(422).text("missing name");
    }
    return HttpResponse::ok().json_of(v.val).status(201);
}

HttpResponse: o modelo de builder

HttpResponse é construído por valor. Cada método do builder recebe self por valor e retorna um HttpResponse novo:

pub fn body(self: HttpResponse, s: string) -> HttpResponse

É por isso que você os encadeia: HttpResponse::ok().text("hi").status(201). Não há mutação no lugar — cada chamada entrega a próxima resposta na cadeia. A própria struct é pequena:

pub struct HttpResponse {
    pub status:        i32,
    pub headers_block: string,
    pub body:          string,
    pub stream_src:    *dyn ChunkSource,  // null for buffered responses
    pub file_path:     string,            // non-empty for the sendfile path
}

Construtores

Você inicia uma resposta com um dos cinco construtores estáticos:

Construtor Status Notas
HttpResponse::ok() 200 Corpo vazio; encadeie .body/.text/… para preenchê-lo.
HttpResponse::not_found() 404 Atalho para with_status(404).
HttpResponse::with_status(code) code O caso geral: qualquer status, sem corpo, sem headers.
HttpResponse::redirect(url) 302 Define Location: url. Encadeie .status(301) para um redirecionamento permanente.
HttpResponse::file(path, mime) 200 sendfile zero-copy do disco; mime se torna o Content-Type.
HttpResponse::ok();
HttpResponse::not_found();
HttpResponse::with_status(204);
HttpResponse::redirect("/login").status(301);
HttpResponse::file("./static/index.html", "text/html; charset=utf-8");

file(...) é especial: o servidor obtém o tamanho do corpo a partir do tamanho do arquivo no disco e transmite os bytes do cache de páginas do kernel diretamente para o socket — o campo body é ignorado nesse caminho. (Mais sobre servir arquivos adiante, na seção de arquivos estáticos.)

Métodos do builder

Uma vez que você tem uma resposta, esses métodos a moldam. Os que definem um corpo também estampam o Content-Type correspondente para você:

Método Define Content-Type? O que faz
body(s) não Define o corpo (e Content-Length). Sem tipo de conteúdo — bytes brutos.
text(s) text/plain; charset=utf-8 Corpo + tipo de conteúdo texto simples.
html(s) text/html; charset=utf-8 Corpo + tipo de conteúdo HTML.
json(s) application/json Corpo a partir de uma string JSON.
json_of(v) application/json Corpo a partir de um *JsonValue, serializado via v.emit().
set(name, value) Define um header, substituindo qualquer linha de mesmo nome existente.
add(name, value) Acrescenta uma linha de header; mantém as linhas de mesmo nome existentes.
status(code) Substitui o código de status.
cookie(name, value) Acrescenta um Set-Cookie: name=value (via add).
header(name) um header de volta, sem distinção de maiúsculas/minúsculas; retorna string.
headers(name) todas as linhas de header correspondentes; retorna *Vector<string>.
stream(source) Transmite o corpo em pedaços a partir de um ChunkSource.

Portanto, text, html, json e json_of são os quatro que definem Content-Type; body deliberadamente não faz isso (você traz seu próprio tipo, ou nenhum). Note que set substitui por nome enquanto add acrescenta — use set para Content-Type, add (ou cookie) para Set-Cookie e Vary, que legitimamente se repetem.

import stdlib::http::*;
import stdlib::json::*;

fn handler(req: *HttpRequest) -> HttpResponse {
    let r: HttpResponse = HttpResponse::ok()
        .body("raw bytes")            // sets Content-Length, no Content-Type
        .text("plain")                // Content-Type: text/plain
        .html("<h1>hi</h1>")          // Content-Type: text/html
        .json("{\"ok\":true}")        // Content-Type: application/json
        .set("X-One", "1")            // replace-by-name header
        .add("Vary", "Accept")        // append header (repeats allowed)
        .add("Vary", "Origin")
        .status(200)
        .cookie("session", "abc123"); // appends a Set-Cookie line

    // json_of takes a *JsonValue and serialises it for you.
    let v: *JsonValue = JsonValue::object();
    v.obj_set("ok", JsonValue::bool(true));
    return HttpResponse::ok().json_of(v);
}

fn main() -> i32 {
    http_listen(8080, handler);
    return 0;
}

Lendo uma resposta de volta

Por que ler sua própria resposta? Middleware e testes precisam disso — para verificar um Content-Type, ou para extrair todas as linhas Set-Cookie que você acrescentou:

import stdlib::http::*;

fn main() -> i32 {
    let resp: HttpResponse = HttpResponse::ok()
        .cookie("session", "abc")
        .cookie("csrf", "xyz")
        .set("Content-Type", "application/json")
        .body("{}");

    let ct: string = resp.header("content-type");   // case-insensitive
    println!("ct =", ct);

    let cookies: *Vector<string> = resp.headers("Set-Cookie");
    for let i: i32 = 0; i < cookies.len(); i++ {
        println!("cookie:", cookies.get(i));
    }
    return 0;
}

header(name) fornece a primeira correspondência (ou ""); headers(name) fornece todas as linhas correspondentes — que é exatamente o que você quer para Set-Cookie. O builder stream(source) para corpos em chunks é coberto em detalhes na seção de streaming e SSE mais adiante no capítulo.

Roteamento com Router

O loop básico http_listen encaminha toda requisição para um único handler. Aplicações reais precisam distribuir por método e caminho: GET /users/:id para uma função, POST /users para outra, todo o resto para um 404. É isso que o Router oferece — uma tabela de despacho ciente de métodos que você monta antecipadamente e depois entrega a um listener.

Router vive em seu próprio módulo, então você precisa tanto dos tipos HTTP principais quanto do import do router:

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

Construindo um router e ouvindo

Um handler é um ponteiro de função simples do tipo fn(*HttpRequest) -> HttpResponse. Você cria um router vazio com Router::new() (retorna um *Router com um handler 404 padrão), registra rotas, e então chama listen:

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");
    return HttpResponse::ok().json("{\"id\":\"".concat(id).concat("\"}"));
}

fn create_user(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::with_status(201).body(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);
    let served: !i32 = r.listen(8080);
    return served.val;
}

new() aloca o router no heap, então combine com defer r.free() para liberar a tabela de rotas ao final do escopo.

listen tem a assinatura:

pub fn listen(self: *Router, port: i32) -> !i32

Retorna um !i32 porque vincular a porta pode falhar (err("bind failed")); em caso de sucesso ele bloqueia para sempre servindo requisições, então na prática o valor Ok só é atingido quando o loop é encerrado. Vincule o resultado (ou o propague com ?) para que o erro não seja descartado silenciosamente.

Sintaxe de padrões

Todo padrão é dividido em / formando segmentos. Cada segmento tem uma de três formas:

Segmento Tipo Corresponde a Captura
/users literal exatamente aquele texto, byte a byte nenhuma
/users/:id parâmetro qualquer segmento de caminho único req.param("id")
/static/*rest curinga o restante do caminho (incluindo vazio) req.param("rest")

Um segmento de parâmetro começa com : e captura um único segmento. Um curinga começa com *, deve ser o último segmento, e captura todo o restante concatenado por / (também corresponde a uma cauda vazia, então /static/*rest corresponde a um /static simples). Você lê ambos via req.param(name), que retorna "" para um nome desconhecido:

// para a rota "/users/:id" correspondida contra /users/42
req.param("id");   // "42"
req.param("zzz");  // ""

Os atalhos de verbo

route(method, pattern, handler) é a primitiva — o método é casado sem distinção de maiúsculas e armazenado em maiúsculas. Os métodos de verbo são wrappers finos em torno dele, e any registra um captura-tudo que corresponde a qualquer verbo (armazena o sentinela ANY_METHOD, ou seja, "*"):

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

fn show(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("show ".concat(req.param("id")));
}

fn serve_asset(req: *HttpRequest) -> HttpResponse {
    let rest: string = req.param("rest");
    return HttpResponse::ok().text("asset: ".concat(rest));
}

fn health(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("ok");
}

fn main() -> i32 {
    let r: *Router = Router::new();
    defer r.free();

    r.get("/items/:id", show);
    r.put("/items/:id", show);
    r.patch("/items/:id", show);
    r.delete("/items/:id", show);
    r.head("/items/:id", show);
    r.options("/items/:id", show);

    r.any("/health", health);           // qualquer método
    r.get("/static/*rest", serve_asset); // cauda curinga

    // Formas de mais baixo nível.
    r.route("GET", "/legacy", health);
    r.route(ANY_METHOD, "/ping", health);

    let served: !i32 = r.listen(8080);
    return served.val;
}
Método Equivalente a
get(p, h) route("GET", p, h)
post(p, h) route("POST", p, h)
put(p, h) route("PUT", p, h)
delete(p, h) route("DELETE", p, h)
patch(p, h) route("PATCH", p, h)
head(p, h) route("HEAD", p, h)
options(p, h) route("OPTIONS", p, h)
any(p, h) route(ANY_METHOD, p, h) — corresponde a qualquer verbo

Despachando diretamente

listen chama dispatch para cada requisição analisada, mas você pode chamá-lo diretamente — ele é a costura para testar roteamento em unidade sem abrir um socket:

pub fn dispatch(self: *Router, req: *HttpRequest) -> HttpResponse

dispatch percorre a tabela de rotas, preenche req.params na primeira correspondência, executa a cadeia de middleware e depois o handler — caindo de volta no handler de não-encontrado quando nada corresponde. Ele muta req.params no lugar, então passe uma requisição recém-construída:

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

fn get_user(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("user ".concat(req.param("id")));
}

fn main() -> i32 {
    let r: *Router = Router::new();
    defer r.free();
    r.get("/users/:id", get_user);

    let req: HttpRequest = HttpRequest {
        method:        "GET",
        path:          "/users/42",
        version:       "HTTP/1.1",
        headers_block: "",
        body:          "",
        params:        null,
        queries:       null,
        tls:           false,
        headers_cache: null,
    };
    let resp: HttpResponse = r.dispatch(&req);
    println!("status:", resp.status);  // 200
    println!("body:", resp.body);      // user 42
    return 0;
}

404 personalizado, sub-routers e estado

Três métodos adicionais do router completam uma aplicação real: not_found_with substitui o handler 404 padrão, scope(prefix, sub) monta as rotas de um sub-router sob um prefixo, e state(p) guarda um ponteiro de estado da aplicação que os handlers acessam posteriormente pelo extrator State<T>.

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

struct Config {
    name: string,
}

fn get_user(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("user ".concat(req.param("id")));
}

fn list_users(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("all users");
}

fn missing(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::with_status(404).json("{\"err\":\"missing\"}");
}

fn main() -> i32 {
    // Um sub-router guarda suas próprias rotas; scope as monta sob um prefixo.
    let api: *Router = Router::new();
    api.get("/users", list_users);
    api.get("/users/:id", get_user);

    let r: *Router = Router::new();
    defer r.free();

    let cfg: *Config = new Config { name: "prod" };
    r.state(cfg as *void);          // acessível via State<Config>

    r.scope("/api/v1", api);        // monta /api/v1/users e /api/v1/users/:id
    r.not_found_with(missing);      // 404 personalizado

    let served: !i32 = r.listen_workers(8080, 4);
    return served.val;
}

scope copia as entradas de rota do sub-router (com o prefixo anteposto), então o sub-router pode ser descartado ou reutilizado depois. Qualquer middleware registrado no sub-router torna-se middleware por-rota que executa após a cadeia do pai e antes do handler — o middleware acumula naturalmente através de escopos aninhados.

Middleware por rota com route_with

A cadeia use_mw(mw) no nível do router executa antes de cada handler. Quando você quer middleware com escopo para uma única rota, use route_with, que recebe um vetor de middlewares para executar entre a cadeia global e o handler:

pub fn route_with(self: *Router, method: string, pattern: string,
                  mws: *Vector<fn(*HttpRequest, *Chain) -> HttpResponse>,
                  handler: fn(*HttpRequest) -> HttpResponse)

Cada middleware tem a forma fn(*HttpRequest, *Chain) -> HttpResponse. Ele chama chain_next(c) para avançar ao próximo middleware (e eventualmente ao handler), ou retorna antecipadamente para curto-circuitar a cadeia:

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

fn auth(req: *HttpRequest, c: *Chain) -> HttpResponse {
    if req.header("Authorization").len() == 0 {
        return HttpResponse::with_status(401).text("no auth");
    }
    return chain_next(c);
}

fn secret(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("top secret");
}

fn main() -> i32 {
    let r: *Router = Router::new();
    defer r.free();

    let mws: *Vector<fn(*HttpRequest, *Chain) -> HttpResponse> = Vector::new();
    mws.push(auth);
    r.route_with("GET", "/secret", mws, secret);

    let served: !i32 = r.listen(8080);
    return served.val;
}

Retornar uma resposta sem chamar chain_next pula o restante do middleware e o handler da rota — o padrão clássico de uma barreira de autenticação que interrompe com 401 antes que o handler seja executado. A próxima seção explora esse padrão por completo.

Middleware

Middleware permite envolver cada requisição com comportamento compartilhado — logging, verificações de autenticação, cabeçalhos CORS, métricas — sem tocar em cada handler. No Glide, um middleware é apenas uma função com uma forma fixa, e o Router encaminha um Chain por eles para que cada um decida se continua ou para.

A assinatura do middleware

Todo middleware é uma função exatamente deste tipo:

fn(*HttpRequest, *Chain) -> HttpResponse

Ela recebe a requisição e um *Chain. Para passar o controle ao próximo middleware (e eventualmente ao handler da rota), chama chain_next(c). Para parar a requisição completamente — por exemplo, rejeitar um chamador não autenticado — simplesmente retorna um HttpResponse sem chamar chain_next. Isso curto-circuita o restante da cadeia e o handler da rota.

O Chain é o contexto por-despacho que o router constrói e percorre para você:

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 avança o índice: de dentro do middleware N ele executa o middleware N+1 e, ao esgotar a lista, invoca o handler da rota. Você nunca constrói um Chain diretamente — você o recebe e o repassa.

Registrando middleware global com use_mw

Router::use_mw adiciona um middleware que executa antes de cada handler correspondido, na ordem de registro:

pub fn use_mw(self: *Router,
              mw: fn(*HttpRequest, *Chain) -> HttpResponse) -> *Router

Aqui está um middleware de logging que registra o método, o caminho e o status final:

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

fn index(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("hello");
}

fn logger(req: *HttpRequest, c: *Chain) -> HttpResponse {
    println!("[", req.method, "] ", req.path);
    let resp: HttpResponse = chain_next(c);   // executa o restante da cadeia
    println!("  -> ", resp.status);           // inspeciona o resultado
    return resp;
}

fn main() -> i32 {
    let r: *Router = Router::new();
    defer r.free();
    r.use_mw(logger);
    r.get("/", index);
    return 0;
}

use_mw retorna self, então você pode encadear registros se preferir.

Curto-circuito: uma barreira de autenticação

Para rejeitar uma requisição, retorne antecipadamente sem chamar chain_next. Tudo após este middleware — incluindo o handler — é ignorado:

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

fn whoami(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("you are in");
}

fn auth_gate(req: *HttpRequest, c: *Chain) -> HttpResponse {
    let token: string = req.header("Authorization");
    if token.len() == 0 {
        // Sem chain_next -> o handler nunca executa.
        return HttpResponse::with_status(401).json("{\"error\":\"unauthorized\"}");
    }
    return chain_next(c);
}

fn main() -> i32 {
    let r: *Router = Router::new();
    defer r.free();
    r.use_mw(auth_gate);
    r.get("/me", whoami);
    return 0;
}

Ordenação

O middleware executa na ordem em que foi registrado. A cadeia se aninha como camadas de uma cebola: o código de cada middleware antes de chain_next executa na entrada, e seu código após chain_next executa na saída (em ordem inversa).

fn first(req: *HttpRequest, c: *Chain) -> HttpResponse {
    println!("first: before");
    let resp: HttpResponse = chain_next(c);
    println!("first: after");
    return resp;
}

fn second(req: *HttpRequest, c: *Chain) -> HttpResponse {
    println!("second: before");
    let resp: HttpResponse = chain_next(c);
    println!("second: after");
    return resp;
}

// r.use_mw(first); r.use_mw(second);

Com first registrado antes de second, uma requisição imprime:

output
first: before
second: before
   <handler runs>
second: after
first: after

Portanto, o middleware registrado primeiro é a camada mais externa — registre preocupações transversais como CORS e logging de requisições cedo.

Middleware de escopo

Router::scope monta um sub-router sob um prefixo (você o encontrou acima). O middleware use_mw do sub-router torna-se middleware por-rota: ele executa após a cadeia do pai e antes do handler. É assim que você aplica autenticação a um grupo de rotas sem afetar o restante.

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

fn get_user(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text(req.param("id"));
}

fn logger(req: *HttpRequest, c: *Chain) -> HttpResponse {
    println!("[", req.method, "] ", req.path);
    return chain_next(c);
}

fn auth_gate(req: *HttpRequest, c: *Chain) -> HttpResponse {
    if req.header("Authorization").len() == 0 {
        return HttpResponse::with_status(401).text("unauthorized");
    }
    return chain_next(c);
}

fn main() -> i32 {
    let api: *Router = Router::new();
    api.use_mw(auth_gate);              // aplica somente dentro do escopo
    api.get("/users/:id", get_user);

    let r: *Router = Router::new();
    defer r.free();
    r.use_mw(logger);                   // aplica a tudo
    r.scope("/api/v1", api);            // logger -> auth_gate -> get_user
    return 0;
}

Uma requisição para /api/v1/users/42 executa logger, depois auth_gate, depois get_user. Escopos se aninham e o middleware acumula à medida que você desce.

CORS como middleware

O módulo stdlib::http::cors fornece um middleware pronto. Você o configura uma vez com install_cors e então registra cors_mw como qualquer outro middleware:

pub struct CorsConfig {
    pub origin:      string,
    pub methods:     string,
    pub headers:     string,
    pub credentials: bool,
    pub max_age:     i32,
}

pub fn install_cors(cfg: CorsConfig)
pub fn cors_mw(req: *HttpRequest, c: *Chain) -> HttpResponse

cors_mw curto-circuita um preflight OPTIONS com 204 e os cabeçalhos configurados, e em qualquer outra requisição executa a cadeia e anexa os cabeçalhos Access-Control-* à resposta na saída. O começo mais rápido é o preset permissivo (CorsConfig::permissive() — origin "*", os verbos comuns, Content-Type, Authorization):

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

fn index(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().json("{\"ok\":true}");
}

fn main() -> i32 {
    install_cors(CorsConfig::permissive());

    let r: *Router = Router::new();
    defer r.free();
    r.use_mw(cors_mw);          // registre cedo para que os cabeçalhos cubram tudo
    r.get("/data", index);
    return 0;
}

Para produção, construa um CorsConfig explicitamente — uma origin específica, os verbos que você realmente permite, credenciais e uma duração de cache para o preflight:

install_cors(CorsConfig {
    origin:      "https://app.example.com",
    methods:     "GET, POST, OPTIONS",
    headers:     "Content-Type, Authorization",
    credentials: true,
    max_age:     3600,
});

Colocando tudo junto

Um servidor típico empilha preocupações globais (CORS, logging) no router raiz e protege um grupo de rotas com um middleware de autenticação de escopo:

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

fn health(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("up");
}

fn secret(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().json("{\"secret\":42}");
}

fn logger(req: *HttpRequest, c: *Chain) -> HttpResponse {
    let resp: HttpResponse = chain_next(c);
    println!(req.method, " ", req.path, " -> ", resp.status);
    return resp;
}

fn require_token(req: *HttpRequest, c: *Chain) -> HttpResponse {
    if req.header("Authorization").len() == 0 {
        return HttpResponse::with_status(401).text("unauthorized");
    }
    return chain_next(c);
}

fn main() -> !i32 {
    install_cors(CorsConfig::permissive());

    let api: *Router = Router::new();
    api.use_mw(require_token);
    api.get("/secret", secret);

    let r: *Router = Router::new();
    defer r.free();
    r.use_mw(cors_mw);          // mais externo: CORS em toda resposta
    r.use_mw(logger);           // depois o logging de requisições
    r.get("/health", health);   // aberto
    r.scope("/api", api);       // /api/secret também executa require_token
    r.listen(8080)?;
    return ok(0);
}

GET /health executa cors_mw -> logger -> health. GET /api/secret executa cors_mw -> logger -> require_token -> secret, retornando 401 quando o cabeçalho Authorization está ausente.

Roteamento declarativo com atributos

A API Router é explícita: você chama Router::new(), registra cada handler com r.get(...) e finaliza com r.listen(...). É exatamente isso que executa, mas também representa muito trabalho de encanamento para o que é, na prática, uma lista de fatos do tipo "esta função responde a GET /users/:id".

O Glide vem com uma DSL de atributos — um conjunto de macros em tempo de compilação em stdlib::http::route — que permite anotar o próprio handler e ter o código de registro gerado automaticamente. Os atributos são açúcar sintático sobre os mesmos métodos do `Router`; nada de mágico acontece em tempo de execução. Entender o que cada um expande é a chave para usá-los com confiança (e para testar rotas unitariamente sem abrir um socket).

Você quase sempre vai combinar o módulo de rotas com os tipos principais e o próprio roteador:

import stdlib::http::*;          // HttpRequest, HttpResponse
import stdlib::http::router::*;  // Router, Chain, chain_next
import stdlib::http::route::*;   // @route/@get/.../@listen, routes!
import stdlib::http::handler::*; // wrapping de @handler usado por @route

Os atributos de rota

@route(METHOD, "/path") é a forma geral. METHOD é um identificador simples (GET, POST, …, sem distinção de maiúsculas — a macro o converte para minúsculas) e o caminho é um literal de string. Existem atributos de açúcar específicos por método que recebem apenas o caminho:

Atributo Expande para Método HTTP
@route(GET, "/p") r.get("/p", handler) como nomeado
@get("/p") r.get("/p", handler) GET
@post("/p") r.post("/p", handler) POST
@put("/p") r.put("/p", handler) PUT
@delete("/p") r.delete("/p", handler) DELETE
@patch("/p") r.patch("/p", handler) PATCH
@head("/p") r.head("/p", handler) HEAD
@options("/p") r.options("/p", handler) OPTIONS
@any("/p") r.any("/p", handler) todo método

O que um atributo de rota realmente emite? No código-fonte (route.glide), a macro impl_route:

  1. Mantém sua função original, renomeada para _user_<name>.
  2. A envolve exatamente como @handler faz — extraindo parâmetros tipados (Path<T>, Query<T>, …) em um simples fn(*HttpRequest) -> HttpResponse.
  3. Emite uma função de registro:
fn _route_register_<name>(r: *Router) {
    r.<method>("<path>", <wrapped_handler>);
}

Então @get("/") em fn index(...) produz um _route_register_index(r) que chama r.get("/", ...). A macro nunca chama listen e nunca constrói um Router — isso é trabalho do @listen ou do routes!.

Duas verificações em tempo de compilação ocorrem durante a expansão (ambas surgem como diagnósticos error:):

  • Formato do caminho (_validate_path): deve começar com /, sem segmentos // vazios.
  • Alinhamento de parâmetros (_validate_path_params): cada segmento :id precisa de um parâmetro id: Path<T> correspondente, e *rest de um rest: Path<string>a menos que o handler receba um único req: *HttpRequest, caso em que você optou por não usar extração tipada e vai ler os parâmetros com req.param("id") por conta própria.

@listen / @listen_workers — gerar main

@listen(port) e @listen_workers(port, n) vão em fn main(r: *Router) { ... }. A macro substitui o corpo de `main` pelo bootstrap completo do servidor (impl_listen em route.glide):

// o que @listen(8080) em `fn main(r: *Router)` expande, aproximadamente:
fn main() -> !i32 {
    let r: *Router = Router::new();
    /* ...seu corpo original de main executa aqui, com `r` no escopo... */
    _route_register_index(r);   // uma chamada por fn com @route no programa
    _route_register_echo(r);
    return r.listen(8080);
}

Coisas que vale saber:

  • O main gerado retorna !i32, então um bind failed de listen se propaga como código de saída diferente de zero. Você não escreve o tipo de retorno por conta própria.
  • Seu próprio corpo (a parte que você escreveu entre as chaves) executa depois de Router::new() e antes do registro, com r já no escopo. Use-o para r.use_mw(...) de middleware global, um r.not_found_with(...) personalizado, etc.
  • Toda função com @route no programa inteiro é registrada automaticamente — você não as lista. Declarar duas rotas com o mesmo method|path é um erro duplicate route em tempo de compilação.
  • @listen(port) chama r.listen(port) (single-threaded). @listen_workers(port, n) chama r.listen_workers(port, n). Escrever @listen_workers(port) sem n usa como padrão 4 workers.

Aqui está um servidor completo. @get e @post anotam os handlers; @listen transforma main no bootstrap. Note o corpo vazio — não há nada a fazer aqui além de escutar.

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::route::*;
import stdlib::http::handler::*;

@get("/")
fn index(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("hello");
}

@post("/echo")
fn echo(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text(req.body);
}

@listen(8080)
fn main(r: *Router) {
}

As assinaturas para as quais esses expandem são os métodos reais do Router:

pub fn get(self: *Router, pattern: string,
           handler: fn(*HttpRequest) -> HttpResponse)
pub fn listen(self: *Router, port: i32) -> !i32
pub fn listen_workers(self: *Router, port: i32, n: i32) -> !i32

@middleware(...) — middleware com escopo de rota

Empilhe um ou mais middlewares em uma única rota adicionando um atributo irmão @middleware(a, b, ...). Os argumentos são idents de funções de middleware — cada uma tem a assinatura fn(*HttpRequest, *Chain) -> HttpResponse e chama chain_next(c) para prosseguir.

Quando um @middleware está presente, o atributo de rota altera o registro emitido de r.<method>(...) para r.route_with(...):

fn _route_register_admin(r: *Router) {
    let mws = Vector::new();
    mws.push(logger);
    mws.push(require_auth);
    r.route_with("GET", "/admin", mws, <wrapped_handler>);
}

Um exemplo completo — logger sempre executa, require_auth curto-circuita com 401 quando não há cabeçalho Authorization:

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::route::*;
import stdlib::http::handler::*;

fn logger(req: *HttpRequest, c: *Chain) -> HttpResponse {
    println!("[", req.method, "] ", req.path);
    return chain_next(c);
}

fn require_auth(req: *HttpRequest, c: *Chain) -> HttpResponse {
    if req.header("Authorization").eq("") {
        return HttpResponse::with_status(401).text("no token");
    }
    return chain_next(c);
}

@get("/admin")
@middleware(logger, require_auth)
fn admin(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("welcome, admin");
}

@listen(8080)
fn main(r: *Router) {
}

routes!(r) — registro explícito (e como testar unitariamente)

@listen é conveniente, mas ele detém o main e abre um socket. Quando você quer registrar em um Router que você construiu — para adicionar middleware personalizado, para compor sub-roteadores, ou acima de tudo para testar rotas sem escutar — use a macro routes!(r) em vez disso.

routes!(r) percorre o programa em busca de toda função com @route e emite as mesmas chamadas _route_register_<name>(r); inline (a mesma verificação de rota duplicada se aplica). Ela não constrói o roteador nem chama listen — você faz os dois. Como o dispatch é apenas r.dispatch(&req) retornando um HttpResponse, você pode construir um HttpRequest à mão e verificar o resultado — sem rede, sem porta. Esta é a forma idiomática de testar handlers unitariamente:

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::route::*;
import stdlib::http::handler::*;
import stdlib::hashmap::*;

@get("/")
fn index(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text("hello");
}

@post("/echo")
fn echo(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::ok().text(req.body);
}

fn main() -> i32 {
    let r: *Router = Router::new();
    routes!(r);
    // r.use_mw(...); // adicione middleware global aqui, se quiser

    // Direcione uma requisição pelo roteador sem socket à vista.
    let req: HttpRequest = HttpRequest {
        method:        "GET",
        path:          "/",
        version:       "HTTP/1.1",
        headers_block: "",
        body:          "",
        params:        null,
        queries:       null,
        tls:           false,
        headers_cache: null,
    };
    let resp: HttpResponse = r.dispatch(&req);
    println!("status:", resp.status);   // 200
    println!("body:", resp.body);       // hello
    return 0;
}

Handlers tipados com @handler

O roteador armazena handlers com um formato fixo: fn(*HttpRequest) -> HttpResponse. Isso é honesto, mas tedioso — cada handler precisa buscar dados da requisição manualmente, parsear o corpo, procurar parâmetros de caminho e construir um objeto de resposta. A macro @handler (de stdlib::http::handler) oferece ergonomia no estilo Axum sobre esse formato: você escreve uma função cujos parâmetros são extratores tipados e cujo retorno é um valor tipado, e a macro lê sua assinatura e emite o código de apoio por você.

Concretamente, @handler faz duas coisas:

  1. Renomeia sua função para _user_<name> (mantendo seu corpo tipado intacto, para que você ainda possa chamá-lo diretamente).
  2. Emite um wrapper chamado <name> assinado exatamente como fn(*HttpRequest) -> HttpResponse, que extrai cada parâmetro da requisição e chama sua função renomeada.

Como o wrapper mantém o nome original e o formato exigido pelo roteador, r.get("/users/:id", get_user) ainda aceita a verificação de tipos sem alargamento.

Um primeiro handler: Path e Json

Aqui está o par mínimo útil — uma leitura por parâmetro de caminho tipado e uma escrita que vincula um corpo JSON e retorna um valor tipado. Note que Path<T> vive em stdlib::http::extract e Json<T> em stdlib::http::typed.

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::handler::*;
import stdlib::http::typed::*;
import stdlib::http::extract::*;
import stdlib::json::*;

pub struct User { pub id: i32 }
impl JsonBind for User {
    fn from_json(v: *JsonValue) -> !User { return ok(User { id: v.get_int("id")? }); }
    fn to_json(self: *User) -> *JsonValue {
        let o: *JsonValue = JsonValue::object();
        o.obj_set("id", JsonValue::int(self.id));
        return o;
    }
}

// GET /users/:id  — o ident do parâmetro `id` é a chave de busca no caminho.
@handler
fn get_user(id: Path<i32>) -> HttpResponse {
    return HttpResponse::ok().text("user #".concat(id.val.to_string()));
}

// POST /users  — corpo vinculado a partir de JSON; retorna Json<User> (serializado automaticamente).
@handler
fn create_user(body: Json<User>) -> Json<User> {
    return Json::wrap(body.val);
}

fn main() -> i32 {
    let r: *Router = Router::new();
    r.get("/users/:id", get_user);
    r.post("/users", create_user);
    return 0;
}

Aqui User é uma struct mínima que implementa JsonBind (from_json / to_json) — essa trait é abordada em profundidade na seção de JSON abaixo. O ponto principal: get_user parece receber um Path<i32>, mas após a macro executar ele é realmente um fn(*HttpRequest) -> HttpResponse, então o roteador o aceita.

Ordem de despacho dos parâmetros

A macro percorre seus parâmetros da esquerda para a direita e escolhe uma estratégia de extração por tipo. A ordem importa porque a primeira regra correspondente vence:

Tipo do parâmetro O que a macro emite Observações
req: *HttpRequest passthrough — req é encaminhado diretamente sem extração alguma
body: Json<T> req.body_json() depois T::from_json(...) embutido; 400 em parse, 422 no bind (T: JsonBind)
name: Path<T> Path::extract(req, "name") ident do parâmetro é a chave de caminho (T: FromPath)
name: Query<T> Query::extract(req, "name") ident do parâmetro é a chave de query (T: FromPath)
state: State<T> State::extract(req) lê o slot definido por Router::state(...)
qualquer outro T T::from_request(req) via a trait FromRequest

Json<T> é mantido como um caso especial manual (em vez de passar por FromRequest) por causa de uma colisão de monomorphização entre o from_request estático de Json<T> e seu método inerente wrap. O comportamento é idêntico do seu lado. O catálogo completo de extratores — Bearer, Headers, Cookies, Form e afins — é o assunto da próxima seção.

Misturando passthrough e extratores

Você pode receber a requisição bruta e parâmetros tipados na mesma assinatura. O passthrough de *HttpRequest é encaminhado sem extração, enquanto os parâmetros tipados são extraídos da requisição:

@handler
fn detail(req: *HttpRequest, id: Path<i32>) -> HttpResponse {
    return HttpResponse::ok().text(
        req.method.concat(" id=").concat(id.val.to_string()));
}

Isso é útil quando você quer principalmente acesso tipado mas ainda precisa de um ou dois campos da requisição bruta.

Formatos de retorno

A macro inspeciona seu tipo de retorno declarado para decidir como finalizar:

Tipo de retorno O que acontece
HttpResponse passthrough — seu valor é retornado como está
Json<T> a macro acrescenta .into_response(), que chama T.to_json() e define Content-Type: application/json

Portanto, retornar Json::wrap(body.val) de um handler -> Json<T> produz automaticamente um 200 com corpo JSON — você nunca toca em HttpResponse para o caminho feliz. A conversão passa pela implementação Json<T> da trait IntoResponse, que é HttpResponse::ok().json_of(self.val.to_json()).

@get / @post fazem o wrapping da mesma forma

Os atributos de roteamento — @get, @post e afins de stdlib::http::route — chamam o mesmo _emit_handler_wrapper internamente. Isso significa que um handler roteado também pode receber parâmetros tipados e retornar Json<T>; você obtém o wrapping de graça, mais o auto-registro via routes!:

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::route::*;
import stdlib::http::typed::*;
import stdlib::http::extract::*;
import stdlib::json::*;

pub struct User { pub id: i32 }
impl JsonBind for User {
    fn from_json(v: *JsonValue) -> !User { return ok(User { id: v.get_int("id")? }); }
    fn to_json(self: *User) -> *JsonValue {
        let o: *JsonValue = JsonValue::object();
        o.obj_set("id", JsonValue::int(self.id));
        return o;
    }
}

@get("/users/:id")
fn show(id: Path<i32>) -> HttpResponse {
    return HttpResponse::ok().text("user #".concat(id.val.to_string()));
}

@post("/users")
fn create(body: Json<User>) -> Json<User> {
    return Json::wrap(body.val);
}

fn main() -> i32 {
    let r: *Router = Router::new();
    routes!(r);          // auto-registra toda fn com @route no programa
    return 0;
}

Se você usar @handler simples com r.get(...) manual, ou @get/@post com routes!, a mecânica de parâmetros tipados e retorno tipado é idêntica.

shell
curl -X POST localhost:8080/users -d 'not json'         # 400 parse error
curl -X POST localhost:8080/users -d '{"name":"x"}'     # 422 missing field
curl localhost:8080/users/abc                           # 400 (Path<i32> não converteu)

Extratores: extraindo dados tipados de uma requisição

Um handler não deveria ter que pescar strings brutas de *HttpRequest manualmente, parsear e decidir qual status retornar quando estão ausentes. É para isso que servem os extratores: cada um sabe como extrair um pedaço específico de dados de uma requisição, convertê-lo para um tipo real do Glide e — quando os dados estão ausentes ou malformados — produzir o status HTTP correto. Você declara extratores como parâmetros do handler e @handler os conecta por você.

Tudo nesta seção vive em um módulo:

import stdlib::http::extract::*;

A trait FromRequest

Todo extrator implementa uma trait. Este é o contrato que @handler chama para cada parâmetro:

pub trait FromRequest {
    /// Constrói `Self` a partir de `req`. A string de Err carrega o status como
    /// `"<code>:<message>"` (ex.: `"401:missing token"`); strings simples
    /// assumem 422 como padrão no wrapper.
    fn from_request(req: *HttpRequest) -> !Self;
}

O retorno é um Result (!Self). Em caso de falha, a string de err funciona duplamente como carregador de status: um extrator a prefixa com "<status>:", ex.: "401:missing token" ou "404:missing path param: id". Quando @handler detecta uma extração falha, alimenta esse err em HttpResponse::from_extract_err, que parseia o prefixo em um status e corpo. Um err simples (sem prefixo) assume como padrão 422 Unprocessable Entity. O próprio *HttpRequest implementa FromRequest (como passthrough), então um handler sempre pode simplesmente pedir a requisição inteira.

Os códigos de status que os extratores embutidos usam:

Código Quando
400 corpo falhou no parse / um valor Path/Query não converteu
422 JSON vinculado mas um campo estava ausente/inválido; também o padrão para erros sem prefixo
401 auth ausente ou incorreta (Bearer, Authorization<S>)
404 um segmento de caminho :name estava ausente
415 Content-Type errado para Form / MultipartForm

Escrevendo seu próprio extrator

Como é apenas uma trait, você pode adicionar um extrator personalizado para seu próprio tipo — digamos, uma verificação de chave de API — e usá-lo como parâmetro de handler exatamente como os embutidos. Os extratores executam na ordem dos parâmetros e curto-circuitam na primeira falha, então codifique o status no prefixo do err:

import stdlib::http::*;
import stdlib::http::extract::*;
import stdlib::http::handler::*;

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

@handler
fn guarded(key: ApiKey) -> HttpResponse {
    return HttpResponse::ok().text("key=".concat(key.key));
}

Você também pode acionar um extrator manualmente — útil dentro de um middleware ou um handler em formato *HttpRequest simples. O padrão espelha o que a macro emite:

fn handle_manually(req: *HttpRequest) -> HttpResponse {
    let r: !Bearer = Bearer::from_request(req);
    if !r.ok { return HttpResponse::from_extract_err(r.err); }
    return HttpResponse::ok().text("ok ".concat(r.val.token));
}

Parâmetros de caminho e query: FromPath

Path<T> e Query<T> são genéricos sobre uma trait de conversão pequena. Onde FromRequest transforma uma requisição inteira em um valor, FromPath transforma um escalar de URL único (um :segment ou um ?key=value) em um valor tipado:

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

A biblioteca padrão fornece três implementações. Suas regras de conversão:

T Aceita Err (→ 400)
string qualquer coisa (identidade) nunca
i32 o que try_parse_int aceita not an int: <s>
bool "true"/"1" e "false"/"0" not a bool: <s>

Tanto Path<T> quanto Query<T> reutilizam FromPath para conversão, então os mesmos três tipos-alvo funcionam em ambas as posições. Quer um tipo personalizado em uma rota? Adicione um impl FromPath for SeuTipo.

O catálogo de extratores

Cada extrator embutido, o que ele lê, como você o acessa e o status que produz quando não consegue:

Tipo Extrai de Acesse via Status em falha
*HttpRequest a requisição inteira o próprio ponteiro nunca (passthrough)
Path<T> um segmento de rota :name, indexado pelo nome do parâmetro .val (tipo T) 404 se ausente, 400 se a conversão T falhar
Query<T> um parâmetro de query ?name=value, indexado pelo nome do parâmetro .val (tipo T) 400 se ausente ou conversão falhar
State<T> o slot de estado compartilhado do roteador (r.state(p)) .val (tipo *T) 500 se nenhum estado foi registrado
Headers o conjunto de cabeçalhos da requisição .get(name) -> string, .has(name) -> bool nunca (cabeçalho ausente → "")
Bearer Authorization: Bearer <token> .token (string) 401 se cabeçalho ausente ou esquema não for Bearer
Basic Authorization: Basic <user:pass> (como um AuthScheme) .user, .pass 401 via o wrapper Authorization<S>
Authorization<S> o cabeçalho Authorization, parseado pelo esquema S .scheme (tipo S) 401 se ausente ou S::parse falhar
Cookies o cabeçalho de requisição Cookie: .get(name) -> string, .has(name) -> bool nunca (cookie ausente → "")
Form um corpo x-www-form-urlencoded .get(name) -> string, .has(name) -> bool 415 Content-Type errado, 400 se o corpo não parsear

Path<T> e Query<T>

import stdlib::http::*;
import stdlib::http::extract::*;
import stdlib::http::handler::*;

// rota /users/:id
@handler
fn get_user(id: Path<i32>) -> HttpResponse {
    return HttpResponse::ok().text("user #".concat(id.val.to_string()));
}

// GET /search?page=2&q=glide
@handler
fn search(page: Query<i32>, q: Query<string>) -> HttpResponse {
    return HttpResponse::ok().text(
        "page=".concat(page.val.to_string()).concat(" q=").concat(q.val));
}

// rota /toggle/:active   ->   /toggle/true
@handler
fn flag(active: Path<bool>) -> HttpResponse {
    if active.val { return HttpResponse::ok().text("on"); }
    return HttpResponse::ok().text("off");
}

Se o segmento de caminho estiver ausente, o handler nunca executa — Path<T> retorna 404. Se estiver presente mas T::from_path rejeitar (ex.: /users/abc para Path<i32>), é um 400. Um parâmetro de query ausente é um 400 (o valor era esperado e não é opcional).

Headers, Bearer, Basic e Authorization<S>

Headers é a forma de menor cerimônia para ler cabeçalhos arbitrários. Ele fixa o ponteiro da requisição e encaminha para req.header(name) — uma busca sem distinção de maiúsculas que retorna "" quando o cabeçalho está ausente:

pub fn get(self: *Headers, name: string) -> string  // sem distinção de maiúsculas, "" se ausente
pub fn has(self: *Headers, name: string) -> bool     // presente e não vazio

Para autenticação, Bearer remove o prefixo Bearer e entrega o token simples em .token. Basic divide user:pass (nota: a v1 não decodifica base64 — trata o valor após Basic como o literal user:pass). Tanto Bearer quanto Basic implementam AuthScheme, então Authorization<Bearer> e Authorization<Basic> fornecem os mesmos dados através de um wrapper uniforme cujo campo .scheme é o valor parseado:

import stdlib::http::*;
import stdlib::http::extract::*;
import stdlib::http::handler::*;

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

@handler
fn echo_ua(headers: Headers) -> HttpResponse {
    if !headers.has("User-Agent") {
        return HttpResponse::with_status(400).text("no UA");
    }
    return HttpResponse::ok().text(headers.get("User-Agent"));
}

@handler
fn login(creds: Authorization<Basic>) -> HttpResponse {
    return HttpResponse::ok().text(
        creds.scheme.user.concat(":").concat(creds.scheme.pass));
}

Um cabeçalho Authorization ausente ou com esquema errado resulta em 401 em todos os casos.

Cookies

Cookies envolve o cabeçalho de requisição Cookie: (somente leitura — para definir um cookie use HttpResponse::cookie(...) na resposta). .get percorre a string do cabeçalho e retorna "" para um cookie ausente; .has é a verificação de presença:

import stdlib::http::*;
import stdlib::http::extract::*;
import stdlib::http::handler::*;

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

Como um cookie ausente é apenas "", Cookies nunca falha na extração — você decide o que um cookie vazio significa e escolhe o status por conta própria.

State<T>: estado compartilhado da aplicação

State<T> fornece a um handler acesso tipado a qualquer coisa que você registrou com Router::state(p) — sem globais, sem captura de closure. O slot de estado armazena um *void; State<T> converte de volta para *T para você, acessível via .val:

impl<T> State<T> {
    pub fn extract(req: *HttpRequest) -> !State<T> { ... }   // 500 se nenhum estado foi definido
}

Como Path<T>, State<T> é tratado como caso especial pela macro, então você apenas declara o parâmetro. Se você esqueceu de chamar r.state(...) antes de escutar, o slot está vazio e a extração retorna 500 (um bug de inicialização, não um erro do cliente).

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::handler::*;
import stdlib::http::extract::*;

pub struct Counter {
    pub hits: i32,
}

@handler
fn stats(state: State<Counter>) -> HttpResponse {
    return HttpResponse::ok().text("hits=".concat(state.val.hits.to_string()));
}

fn main() -> i32 {
    let c: *Counter = new Counter { hits: 0 };
    let r: *Router = Router::new();
    r.state(c as *void);
    r.get("/stats", stats);
    return 0;
}

Juntando tudo: State<T> + Bearer + Query<T>

Um handler pode misturar quantos extratores quiser; @handler os executa em ordem e curto-circuita para o status da primeira falha. Aqui um único handler lê o estado compartilhado, valida um bearer token e parseia um parâmetro de query — tudo tipado:

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::extract::*;
import stdlib::http::handler::*;

struct Db(name: string)

@handler
fn list_users(state: State<Db>, auth: Bearer, page: Query<i32>) -> HttpResponse {
    if auth.token.eq("") {
        return HttpResponse::with_status(403).text("forbidden");
    }
    return HttpResponse::ok().text(
        "db=".concat(state.val.name)
            .concat(" page=").concat(page.val.to_string()));
}

fn main() -> i32 {
    let r: *Router = Router::new();
    let db: *Db = malloc(sizeof(Db)) as *Db;
    db.name = "users";
    r.state(db as *void);
    return 0;
}

Se a requisição não tiver Authorization: Bearer …, o corpo do handler nunca executa e o cliente recebe 401 — direto do extrator Bearer. Se ?page= estiver ausente, é um 400. Se o estado nunca foi registrado, 500. Cada extrator detém seu próprio modo de falha, então o corpo do seu handler só vê dados totalmente válidos e tipados. (Form e MultipartForm também são extratores — eles recebem tratamento próprio na seção de arquivos estáticos e uploads.)

Trabalhando com corpos JSON

A maioria das APIs JSON faz a mesma dança em toda requisição: analisa o corpo, verifica se ele tem o formato esperado, extrai os campos e serializa um valor tipado na saída. O Glide oferece um trait — JsonBind — e um wrapper fino — Json<T> — que transformam essa cerimônia em algumas linhas. Você ensina ao compilador como seu struct transita de e para JSON uma única vez, e a maquinaria de handlers tipados faz o resto.

Tudo aqui é construído sobre a árvore de valores JSON brutos de stdlib::json (o tipo JsonValue com seus acessores get_string / get_int / obj_set / emit). Em resumo: JsonValue é uma união marcada (kind é uma das constantes JSON_*), e os acessores tipados retornam !T / ?T para que você possa propagar ou usar um valor padrão de forma limpa.

O trait JsonBind

JsonBind vive em stdlib::http::typed e é o contrato para "este struct sabe como ler e escrever a si mesmo como JSON":

pub trait JsonBind {
    fn from_json(v: *JsonValue) -> !Self;
    fn to_json(self: *Self) -> *JsonValue;
}
  • from_json recebe um *JsonValue já analisado e retorna !Selfok(value) em caso de sucesso, err("…") quando o JSON tem o formato errado. A string de erro é o que o cliente verá como corpo do 422.
  • to_json recebe self por ponteiro e constrói um *JsonValue para emitir.

Você implementa ambos manualmente. Dentro de from_json, normalmente você verifica o tipo e depois usa os acessores tipados do objeto, propagando falhas de campo com ?:

Acessor Retorna Em ausência / tipo errado
v.get_string("k") !string err (use com ?)
v.get_int("k") !i32 err (use com ?)
v.get_bool("k") !bool err (use com ?)
v.get_float("k") !f64 err (int promovido a f64)
v.opt_string("k") ?string none()
v.opt_int("k") ?i32 none()
v.get_object("k") !*JsonValue err — perfure mais fundo
v.get_array("k") !*JsonValue err — percorra com len()/at(i)
v.get_any("k") *JsonValue null — passagem dinâmica

Uma string obrigatória com um inteiro opcional ao lado:

import stdlib::http::*;
import stdlib::http::typed::*;
import stdlib::json::*;

pub struct Pet {
    pub name: string,
    pub age:  ?i32,
}

impl JsonBind for Pet {
    fn from_json(v: *JsonValue) -> !Pet {
        if v.kind != JSON_OBJECT { return err("expected object"); }
        return ok(Pet {
            name: v.get_string("name")?,   // obrigatório: 422 se ausente
            age:  v.opt_int("age"),         // opcional: none() se ausente
        });
    }

    fn to_json(self: *Pet) -> *JsonValue {
        let v: *JsonValue = JsonValue::object();
        v.obj_set("name", JsonValue::string(self.name));
        if self.age.has {
            v.obj_set("age", JsonValue::int(self.age.val));
        }
        return v;
    }
}

Json::wrap, json_respond e o caminho IntoResponse

Uma vez que um tipo implementa JsonBind, três peças transformam um valor em uma resposta. Json<T> é o portador; o resto é açúcar sintático sobre ele:

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

impl<T: JsonBind> Json<T> {
    pub fn wrap(v: T) -> Json<T> { /* Json { val: v } */ }
}

pub fn json_respond<T: JsonBind>(v: T) -> HttpResponse { /* wrap + into_response */ }

A conversão para um HttpResponse passa pelo trait IntoResponse, também em stdlib::http::typed:

impl<T: JsonBind> IntoResponse for Json<T> {
    fn into_response(self: Json<T>) -> HttpResponse {
        return HttpResponse::ok().json_of(self.val.to_json());
    }
}

json_of (o builder de HttpResponse que você viu antes) define Content-Type: application/json e emite a árvore to_json() do valor como corpo, então uma resposta Json<T> é sempre um 200 com o header correto. IntoResponse também é implementado para o próprio HttpResponse (identidade), o que é a razão pela qual um handler pode mesclar respostas tipadas e brutas em branches diferentes. Estas três formas são equivalentes — escolha por gosto:

return json_respond(pet);                       // mais curto
return Json::wrap(pet).into_response();          // explícito
return HttpResponse::ok().json_of(pet.to_json()); // totalmente manual

Juntar binding e resposta em um handler simples fn(*HttpRequest) -> HttpResponse mostra de onde vem cada código de status. req.body_json() retorna !*JsonValueerr em um corpo vazio ou malformado), então você mapeia isso para 400, e um bind from_json falho para 422:

fn create_pet(req: *HttpRequest) -> HttpResponse {
    let body_r: !*JsonValue = req.body_json();
    if !body_r.ok {
        return HttpResponse::with_status(400).text(body_r.err);   // JSON inválido
    }
    let pet_r: !Pet = Pet::from_json(body_r.val);
    if !pet_r.ok {
        return HttpResponse::with_status(422).text(pet_r.err);    // formato inválido
    }
    let pet: Pet = pet_r.val;
    return json_respond(pet);
}

Esse dois passos (400 depois 422) é o padrão canônico — e é exatamente o que a macro @handler gera para você quando um parâmetro é tipado como Json<T>.

Handlers tipados: Json<T> na entrada, Json<T> na saída

Escrever a escada 400/422 manualmente em cada endpoint cansa. Quando um parâmetro é tipado como Json<T>, o wrapper @handler executa exatamente a cerimônia acima — req.body_json() (400 em falha de análise), T::from_json(...) (422 em falha de bind), e então passa o valor tipado para sua função. Quando o tipo de retorno é Json<T>, o wrapper acrescenta .into_response(), então você apenas retorna Json::wrap(value) e obtém um 200 application/json.

Aqui está um endpoint @post completo que recebe um corpo tipado e ecoa uma resposta tipada, conectado a um servidor com @listen:

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::route::*;
import stdlib::http::handler::*;
import stdlib::http::typed::*;
import stdlib::json::*;

pub struct CreateUser {
    pub name: string,
    pub age:  i32,
}

impl JsonBind for CreateUser {
    fn from_json(v: *JsonValue) -> !CreateUser {
        if v.kind != JSON_OBJECT { return err("expected object"); }
        return ok(CreateUser {
            name: v.get_string("name")?,
            age:  v.get_int("age")?,
        });
    }

    fn to_json(self: *CreateUser) -> *JsonValue {
        let v: *JsonValue = JsonValue::object();
        v.obj_set("name", JsonValue::string(self.name));
        v.obj_set("age",  JsonValue::int(self.age));
        return v;
    }
}

// O corpo é analisado como Json<CreateUser> (400/422 tratados pela macro).
// O retorno Json<CreateUser> é auto-convertido via IntoResponse para um
// 200 com Content-Type: application/json.
@handler
@post("/users")
fn create_user(body: Json<CreateUser>) -> Json<CreateUser> {
    let u: CreateUser = body.val;
    return Json::wrap(CreateUser { name: u.name, age: u.age });
}

@listen(8080)
fn main() {
}

Um round-trip contra ele:

shell
curl -s -X POST localhost:8080/users \
  -H 'content-type: application/json' \
  -d '{"name":"alice","age":30}'
# {"name":"alice","age":30}

Os códigos de status saem do corpo de graça:

shell
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/users -d 'not json'
# 400   (body_json falhou ao analisar)

curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/users \
  -H 'content-type: application/json' -d '{"name":"alice"}'
# 422   (from_json: "age" obrigatório ausente)

Coleções: Vector<T> tem JsonBind de graça

Você não precisa implementar JsonBind para uma lista de valores bindáveis — a stdlib vem com uma impl genérica: qualquer Vector<T> onde T: JsonBind é em si JsonBind. Ela mapeia um array JSON para um Vector<T> e vice-versa, propagando o erro do primeiro elemento inválido com ? (sem pular silenciosamente; entrada que não é array gera erro com "expected array").

let xs: *Vector<Item> = Vector::new();
xs.push(Item { sku: "a1", qty: 2 });
xs.push(Item { sku: "b2", qty: 5 });

let s: string = (xs).to_json().emit();
// [{"sku":"a1","qty":2},{"sku":"b2","qty":5}]

let back: Vector<Item> = Vector::from_json(JsonValue::parse(s))?;

Isso significa que um handler retornando Json<Vector<Item>> (com @handler) ou chamando json_respond(items) serializa um array JSON sem código extra — contanto que o tipo do elemento faça o bind.

Respostas em streaming e Server-Sent Events

Nem toda resposta cabe na memória. Uma exportação de vários gigabytes, um log em tempo real ou um feed de chat deve sair do servidor conforme é produzido, em vez de ser bufferizado em uma grande string body. O Glide lida com isso por meio de chunked transfer encoding: em vez de definir um corpo, você passa à resposta uma fonte que o servidor consulta para obter a próxima fatia de bytes, enquadrando cada uma como um chunk de Transfer-Encoding: chunked no fio.

Server-Sent Events (SSE) são uma camada fina e padronizada sobre esse mesmo mecanismo — um text/event-stream unidirecional que os navegadores consomem com a API EventSource.

O trait ChunkSource

Corpos em streaming são conduzidos por um trait de método único em stdlib::http:

pub trait ChunkSource {
    fn next_chunk(self: *Self) -> ?string;
}

O servidor chama next_chunk repetidamente. Cada some(s) é gravado como um chunk; none() encerra o stream. O Glide não tem closures, então a fonte mantém seu próprio estado em um struct — tipicamente um cursor mais o que quer que ela itere — e a impl avança esse estado a cada chamada.

Você anexa uma fonte com HttpResponse.stream:

pub fn stream(self: HttpResponse, source: *dyn ChunkSource) -> HttpResponse;

Observe o tipo do parâmetro: *dyn ChunkSource. Você passa um ponteiro para seu struct concreto, convertido com as *dyn ChunkSource. Uma vez que uma fonte de stream é definida, o body bufferizado é ignorado e o servidor emite chunked encoding em vez de Content-Length.

Aqui está um servidor completo que transmite três linhas, um chunk cada:

import stdlib::http::*;

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 report(_req: *HttpRequest) -> HttpResponse {
    let items: *Vector<string> = Vector::new();
    items.push("alpha");
    items.push("beta");
    items.push("gamma");
    let src: *Lines = malloc(sizeof(Lines)) as *Lines;
    src.items = items;
    src.idx = 0;
    return HttpResponse::ok()
        .set("Content-Type", "text/plain")
        .stream(src as *dyn ChunkSource);
}

fn main() -> !i32 {
    http_listen_workers_blocking(8080, 4, report)?;
    return ok(0);
}

Por que handlers com streaming devem rodar no servidor bloqueante

Um handler com streaming não termina quando retorna o HttpResponse. O servidor então fica em loop bombeando next_chunk até ver none(), e sua fonte pode bloquear entre uma chamada e outra — esperando em um canal, um timer, ou novas linhas de log. Esse comportamento de espera é incompatível com o caminho rápido de máquina de estados não bloqueante.

Execute handlers de streaming e SSE no servidor de corrotina por conexão:

pub fn http_listen_workers_blocking(port: i32, n: i32,
                                    handler: fn(*HttpRequest) -> HttpResponse) -> !i32;

(Router expõe a mesma ideia como r.listen_workers_blocking(...).) Usar o http_listen simples para uma fonte bloqueante vai travar o event loop.

Server-Sent Events

SSE vive em stdlib::http::sse. Um evento mapeia diretamente para o formato do fio:

pub struct SseEvent {
    pub event: string,   // linha `event:` — nome opcional
    pub id:    string,   // linha `id:` — opcional, ecoado de volta como Last-Event-ID
    pub data:  string,   // payload `data:` (multi-linha permitido)
    pub retry: i32,      // `retry:` ms antes de reconexão do cliente (0 = omitir)
}

Dois construtores cobrem os casos comuns:

Construtor Constrói Formato no fio
SseEvent::data(payload) evento apenas com data data: payload\n\n
SseEvent::named(name, payload) evento nomeado event: name\ndata: payload\n\n

Para controle total (id, retry) construa o literal do struct diretamente.

Helpers de formatação

Você raramente os chama manualmente ao usar sse_response — a fonte o faz — mas eles são os blocos de construção:

Função Finalidade Saída
sse_format(e: SseEvent) -> string serializa um evento para bytes do fio event: tick\ndata: 1\n\n
sse_comment(text: string) -> string linha de comentário / heartbeat : text\n\n
sse_keepalive() -> string frame mínimo de heartbeat :\n\n
sse_last_event_id(req: *HttpRequest) -> string header Last-Event-ID do cliente (vazio se ausente)

sse_format emite uma linha data: por \n no payload (conforme a spec; o navegador as une novamente), e um evento vazio serializa para uma única linha em branco.

sse_response

Envolva um ChunkSource em uma resposta SSE totalmente configurada com uma chamada:

pub fn sse_response(source: *dyn ChunkSource) -> HttpResponse;

Ela define os headers que SSE requer para você:

output
Content-Type: text/event-stream
Cache-Control: no-cache
X-Accel-Buffering: no      (desabilita o buffering de proxy do nginx)
Connection: keep-alive

Um endpoint SSE orientado por produtor

O formato mais comum: um struct que gera o próximo evento a cada chamada. Este emite cinco eventos tick e suporta reconexão via Last-Event-ID — quando um navegador reconecta após uma queda, ele envia de volta o último id: que viu, para que você possa retomar.

import stdlib::http::*;
import stdlib::http::sse::*;

pub struct Pulse {
    pub idx:   i32,
    pub total: i32,
}

impl ChunkSource for Pulse {
    fn next_chunk(self: *Pulse) -> ?string {
        if self.idx >= self.total { return none(); }
        self.idx = self.idx + 1;
        return some(sse_format(SseEvent::named("tick", self.idx.to_string())));
    }
}

fn ticks(req: *HttpRequest) -> HttpResponse {
    let last: string = sse_last_event_id(req);
    let start: i32 = if last.len() > 0 { last.try_parse_int() ?? 0 } else { 0 };

    let p: *Pulse = malloc(sizeof(Pulse)) as *Pulse;
    p.idx = start;
    p.total = start + 5;
    return sse_response(p as *dyn ChunkSource);
}

fn main() -> !i32 {
    http_listen_workers_blocking(8080, 4, ticks)?;
    return ok(0);
}

No lado do cliente, isso é apenas:

shell
curl -N http://localhost:8080/   # -N desabilita o buffering do curl
output
event: tick
data: 1

event: tick
data: 2
...

Fan-out com SseChannel

Quando os eventos se originam em outro lugar na sua aplicação — uma transmissão pub/sub, um feed de progresso de job — envolva um canal em vez de gerar eventos inline. SseChannel é um ChunkSource que bloqueia em recv e produz cada evento que um remetente envia:

pub struct SseChannel {
    pub ch: *chan<SseEvent>,
}

impl SseChannel {
    pub fn new(ch: *chan<SseEvent>) -> *SseChannel;
}

Seu next_chunk recebe um evento, o formata e retorna none() quando vê um sentinela cujo nome de event é igual à constante SSE_END — é assim que um produtor sinaliza o fim do stream:

pub const SSE_END: string = "__close__";
import stdlib::http::*;
import stdlib::http::sse::*;

fn stream(_req: *HttpRequest) -> HttpResponse {
    let ch: *chan<SseEvent> = malloc(sizeof(chan<SseEvent>)) as *chan<SseEvent>;
    *ch = make_chan(64);
    ch.send(SseEvent::data("hello"));
    ch.send(SseEvent::named("chat", "world"));
    // Um sentinela cujo nome == SSE_END diz à fonte para parar.
    ch.send(SseEvent { event: SSE_END, id: "", data: "", retry: 0 });
    let src: *SseChannel = SseChannel::new(ch);
    return sse_response(src as *dyn ChunkSource);
}

fn main() -> !i32 {
    http_listen_workers_blocking(8080, 4, stream)?;
    return ok(0);
}

Em uma configuração de broadcast real, o canal é criado uma vez e compartilhado, e outras corrotinas enviam eventos para ele enquanto o handler mantém a extremidade de leitura. A requisição permanece aberta até que o canal entregue um evento SSE_END (ou o cliente desconecte, o que derruba o escritor corrente abaixo).

Arquivos estáticos, uploads e formulários

A maioria dos serviços reais faz mais do que emitir JSON: eles servem um bundle CSS, aceitam uma foto de perfil, processam um formulário de login e comprimem grandes respostas no fio. A stdlib HTTP do Glide cobre todos os quatro com peças pequenas e focadas — serve_dir para diretórios inteiros, HttpResponse::file para arquivos individuais, o builder/parser Multipart para uploads, o extrator Form para corpos urlencoded, e gzip_mw para compressão.

Servindo um diretório inteiro com serve_dir

Para expor um diretório de assets sob um prefixo de URL, monte-o uma vez na inicialização. A configuração é um struct simples:

// de stdlib::http::static
pub struct StaticOpts {
    pub root:          string,   // diretório no disco
    pub url_prefix:    string,   // segmento de URL sob o qual os arquivos ficam
    pub cache_control: string,   // header Cache-Control (vazio = omitir)
}

pub fn serve_dir(r: *Router, opts: StaticOpts) { /* ... */ }

root é o diretório no disco; url_prefix é o caminho de URL que mapeia para ele. Com url_prefix: "/static" e root: "./public", um GET /static/css/site.css./public/css/site.css. cache_control é emitido literalmente como header Cache-Control da resposta quando não está vazio — passe "public, max-age=31536000, immutable" para bundles com fingerprint, ou "no-cache" para forçar revalidação.

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",
    });
    r.listen_workers(8080, 4);
    return 0;
}

Internamente, serve_dir registra uma única rota wildcard — GET /<url_prefix>/*filepath — e serve arquivos pelo mesmo caminho de cópia zero HttpResponse::file descrito abaixo. Você obtém o básico de produção de graça: proteção contra path-traversal (requisições contendo segmentos .., uma / inicial ou um byte NUL recebem um 403), um ETag fraco derivado do tamanho do arquivo para que navegadores possam revalidar com If-None-Match e receber um 304, e resolução de index.html para caminhos de diretório.

Enviando um único arquivo com HttpResponse::file

Quando você quer que um handler retorne um arquivo específico — um relatório gerado, um download, um index.html com template — construa a resposta diretamente. Você fornece o tipo MIME por conta própria:

// de stdlib::http
pub fn file(path: string, mime: string) -> HttpResponse
import stdlib::http::*;
import stdlib::http::router::*;

fn download(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::file("./reports/q1.pdf", "application/pdf");
}

fn home(req: *HttpRequest) -> HttpResponse {
    return HttpResponse::file("./public/index.html", "text/html; charset=utf-8");
}

fn main() -> i32 {
    let r: *Router = Router::new();
    r.get("/report", download);
    r.get("/", home);
    r.listen(8080);
    return 0;
}

O corpo é transmitido diretamente do disco (sendfile/TransmitFile), então o arquivo nunca passa por uma string do Glide. serve_dir escolhe o tipo MIME pela extensão do arquivo para você; com HttpResponse::file você o escolhe explicitamente.

Construindo um corpo multipart

Para enviar um arquivo — de um cliente Glide, um teste, ou uma chamada serviço-a-serviço — monte um corpo multipart/form-data com o builder Multipart:

// de stdlib::http::multipart
impl Multipart {
    pub fn new() -> *Multipart                       // boundary aleatório de 24 caracteres
    pub fn with_boundary(boundary: string) -> *Multipart
    pub fn add_field(self: *Multipart, name: string, value: string)
    pub fn add_file(self: *Multipart, name: string, filename: string,
                    content_type: string, data: string)
    pub fn content_type(self: *Multipart) -> string  // "multipart/form-data; boundary=..."
    pub fn body(self: *Multipart) -> string
}

body() serializa cada parte no formato do fio RFC 7578; content_type() retorna o header correspondente (com o boundary) para que o receptor saiba onde as partes se dividem. Passe ambos ao cliente HTTP:

import stdlib::http::multipart::*;
import stdlib::http::client::*;

fn main() -> i32 {
    let mp: *Multipart = Multipart::new();
    mp.add_field("user", "alice");
    mp.add_field("note", "hello world");
    mp.add_file("avatar", "avatar.png", "image/png", "\x89PNG fake bytes");

    let body: string = mp.body();
    let ct: string = mp.content_type();

    let c: *HttpClient = HttpClient::new();
    c.post("https://example.com/upload", body, ct);
    return 0;
}

new() escolhe um boundary aleatório de 24 caracteres, que é o que você quer em produção — é muito mais longo do que qualquer corpo de parte plausível, então não vai colidir com seus dados. Para testes onde você verifica os bytes exatos, fixe o boundary com with_boundary:

import stdlib::http::multipart::*;

fn main() -> i32 {
    let mp: *Multipart = Multipart::with_boundary("XYZ");
    mp.add_field("k", "v");
    let body: string = mp.body();
    // body agora começa com "--XYZ\r\n..."
    println!(body.starts_with("--XYZ\r\n"));
    return 0;
}

Analisando uploads com MultipartForm

No lado do recebimento, declare um parâmetro MultipartForm em um @handler e o framework analisa o corpo da requisição para você — o mesmo mecanismo FromRequest que Bearer, Headers e seus similares usam. Dois structs carregam o resultado:

// de stdlib::http::multipart
pub struct UploadedFile {
    pub name:         string,   // nome do campo do formulário
    pub filename:     string,   // nome do arquivo fornecido pelo cliente
    pub content_type: string,   // Content-Type da parte (padrão application/octet-stream)
    pub body:         string,   // bytes brutos do arquivo
}

pub struct MultipartForm {
    pub fields: *HashMap<string>,        // partes não-arquivo, indexadas pelo nome do campo
    pub files:  *Vector<UploadedFile>,   // cada parte de arquivo
}

Partes de texto simples vão para fields; partes com filename= vão para files:

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::handler::*;
import stdlib::http::multipart::*;

@handler
fn upload(mp: MultipartForm) -> HttpResponse {
    if mp.files.len() > 0 {
        let f: UploadedFile = mp.files.get(0);
        println!("got", f.filename, f.content_type, f.body.len());
        // f.name      -> o nome do campo do formulário
        // f.filename  -> o nome do arquivo fornecido pelo cliente
        // f.body      -> bytes brutos do arquivo
    }
    // Partes não-arquivo ficam no mapa de fields.
    if mp.fields.contains("user") {
        println!("user", mp.fields.get("user"));
    }
    return HttpResponse::ok().text("uploaded");
}

fn main() -> i32 {
    let r: *Router = Router::new();
    r.post("/upload", upload);
    r.listen(8080);
    return 0;
}

Se a requisição não for um upload válido, o extrator interrompe com um 400 antes que seu handler execute. A string de erro carrega o prefixo de status que a macro @handler consome:

String de erro Quando
400:not multipart Content-Type não é multipart/form-data
400:no boundary Content-Type é multipart mas não tem boundary=
400:malformed o corpo não termina de forma limpa

O extrator Form para corpos urlencoded

Para posts de formulários HTML clássicos (application/x-www-form-urlencoded), use o extrator Form em vez disso. Ele expõe acessores simples:

// de stdlib::http::extract
impl Form {
    pub fn get(self: *Form, name: string) -> string  // primeiro valor, "" se ausente
    pub fn has(self: *Form, name: string) -> bool     // true se presente
}
import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::handler::*;
import stdlib::http::extract::*;

@handler
fn login(form: Form) -> HttpResponse {
    let user: string = form.get("user");
    if !form.has("pass") {
        return HttpResponse::with_status(400).text("missing password");
    }
    return HttpResponse::ok().text("welcome, ".concat(user));
}

fn main() -> i32 {
    let r: *Router = Router::new();
    r.post("/login", login);
    r.listen(8080);
    return 0;
}

get retorna o primeiro valor de uma chave (ou "" quando ausente); has distingue presença de um valor vazio. Uma requisição cujo Content-Type não é application/x-www-form-urlencoded é rejeitada com 415 antes que o handler execute. A maquinaria de análise vive em stdlib::http::form, onde FormData é uma lista ordenada de pares (name, value) — chaves duplicadas são mantidas — que você pode construir, codificar para a string do fio e decodificar de volta:

import stdlib::http::form::*;

fn build_and_parse() -> !i32 {
    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);
    let data: *FormData = r?;
    for let i: i32 = 0; i < data.len(); i++ {
        println!(data.name_at(i), "=", data.value_at(i));
    }
    return ok(0);
}

Compressão com gzip_mw

Respostas de texto grandes (HTML, JSON, bundles JS) comprimem bem. Monte o middleware gzip no router e ele comprime respostas qualificadas de forma transparente:

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

fn big(req: *HttpRequest) -> HttpResponse {
    let mut s: string = "";
    for let i: i32 = 0; i < 200; i++ {
        s = s.concat("the quick brown fox jumps over the lazy dog\n");
    }
    return HttpResponse::ok().text(s);
}

fn main() -> i32 {
    let r: *Router = Router::new();
    r.use_mw(gzip_mw);
    r.get("/big", big);
    r.listen(8080);
    return 0;
}

gzip_mw executa o restante da cadeia, então comprime o corpo com gzip e define Content-Encoding: gzip — mas somente quando vale a pena. Ele pula a compressão a menos que todos esses critérios sejam atendidos:

  • a requisição anunciou Accept-Encoding: gzip,
  • o status da resposta é 2xx,
  • a resposta ainda não carrega um Content-Encoding,
  • o corpo tem pelo menos 1024 bytes (o overhead supera o ganho em payloads pequenos),
  • o Content-Type não é um formato já comprimido (image/*, video/*, audio/*, application/zip, application/gzip, application/octet-stream).

O cliente HTTP

Até aqui, este capítulo tratou de servir requisições. A outra metade da história é fazê-las. O Glide inclui um cliente HTTP/1.1 (com HTTP/2-sobre-TLS de forma transparente quando o servidor negocia) em stdlib::http::client. Tudo que você precisa fica atrás de um único import:

import stdlib::http::client::*;

Duas camadas se empilham uma sobre a outra. No topo estão funções livres de uso único para o caso comum — busca uma URL, posta algum JSON, pronto. Abaixo delas fica HttpClient, um objeto de configuração reutilizável que você mantém para compartilhar padrões (user agent, timeouts, cookies, política de redirecionamento). E na base está HttpClientRequest, um envelope que você monta manualmente quando precisa de controle total sobre o método, os cabeçalhos e o corpo.

Toda chamada retorna a mesma coisa: !*HttpResponse. Isso é um Result envolvendo um ponteiro para a mesma struct HttpResponse que o lado servidor produz, então você a lê com os mesmos .status, .body e .header(...) que já conhece.

Helpers de uso único

Para uma única requisição, pule a cerimônia. Duas funções livres criam um cliente padrão, disparam a requisição e destroem o cliente por você:

pub fn http_get(url: string) -> !*HttpResponse
pub fn http_post_json(url: string, body: string) -> !*HttpResponse

http_get faz um GET simples. http_post_json envia body via POST com Content-Type: application/json — você mesmo serializa o JSON (via JsonValue::emit ou um literal de string), o helper apenas define o cabeçalho.

O retorno é um Result, então trate os dois modos de falha. O ! externo é uma falha de transporte — DNS, conexão recusada, timeout, resposta malformada. Um status HTTP não-2xx não é um erro; ele chega como uma resposta ok perfeitamente válida com .status definido para o que o servidor informou. Portanto, você verifica .ok primeiro e depois inspeciona .status:

import stdlib::http::*;
import stdlib::http::client::*;

fn main() -> i32 {
    let r: !*HttpResponse = http_get("http://example.com/");
    if r.ok {
        let resp: *HttpResponse = r.val;
        println!("status:", resp.status);
        println!(resp.body.len(), "bytes");
    } else {
        println!("request failed:", r.err);
    }
    return 0;
}

Quando você está dentro de uma função que ela própria retorna um Result, o operador pós-fixo ? colapsa a verificação de transporte em um único caractere — ele desempacota r.val em caso de sucesso e retorna antecipadamente o err caso contrário. Isso deixa você livre para ramificar no status:

import stdlib::http::*;
import stdlib::http::client::*;

// Propaga erros de transporte com `?`, depois ramifica no status.
fn fetch_title() -> !string {
    let resp: *HttpResponse = http_get("http://example.com/")?;
    if resp.status == 200 {
        return ok(resp.body);
    }
    return err(format!("unexpected status {}", resp.status));
}

fn main() -> !i32 {
    let body: string = fetch_title()?;
    println!("got", body.len(), "bytes");
    return ok(0);
}

A struct HttpClient

Os helpers de uso único criam um cliente novo a cada chamada, o que significa nenhum cookie compartilhado, nenhum reuso de conexão ou configuração, e configurações padrão toda vez. Quando você faz mais de uma requisição — ou precisa de um user agent personalizado, um timeout, ou um jar de cookies — crie um único HttpClient e reutilize-o:

pub fn new() -> *HttpClient

HttpClient::new() retorna um *HttpClient bruto com padrões sensatos; você ajusta os campos públicos antes de emitir requisições:

Campo Tipo Padrão O que faz
user_agent string "glide/0.1" Enviado como cabeçalho User-Agent em toda requisição.
max_redirects i32 5 Quantos saltos 3xx os métodos do/get/post seguirão antes de desistir.
jar *CookieJar null Atribua um CookieJar::new() para ingerir automaticamente Set-Cookie e reenviar Cookie: entre requisições.
tls_insecure bool false Ignora a validação do certificado TLS. Apenas para desenvolvimento local — não use em produção.
timeout_ms i32 0 Timeout total por requisição (conexão + envio + recebimento); 0 desativa.
http2 bool false Tenta HTTP/2 via ALPN sobre TLS; faz fallback para HTTP/1.1 de forma transparente.

Os métodos de requisição compartilham a assinatura -> !*HttpResponse:

Método Assinatura
get get(self, url: string) -> !*HttpResponse
post post(self, url: string, body: string, content_type: string) -> !*HttpResponse
post_json post_json(self, url: string, body: string) -> !*HttpResponse
put put(self, url: string, body: string, content_type: string) -> !*HttpResponse
delete delete(self, url: string) -> !*HttpResponse
do do(self, req: *HttpClientRequest) -> !*HttpResponse
do_once do_once(self, req: *HttpClientRequest) -> !*HttpResponse

post_json é apenas post(url, body, "application/json") — uma conveniência para o tipo de conteúdo mais comum. Veja um cliente configurado postando JSON e lendo a resposta de volta, incluindo os cabeçalhos:

import stdlib::http::*;
import stdlib::http::client::*;

fn main() -> !i32 {
    // Reutiliza um cliente em várias chamadas; personaliza os padrões primeiro.
    let c: *HttpClient = HttpClient::new();
    c.user_agent = "myapp/1.0";
    c.timeout_ms = 5000;
    c.max_redirects = 3;

    let r: !*HttpResponse = c.post_json("http://localhost:8080/users",
                                        "{\"name\":\"alice\"}");
    if r.ok {
        let resp: *HttpResponse = r.val;
        println!("status:", resp.status);
        let ct: string = resp.header("Content-Type");
        println!("content-type:", ct);
        println!(resp.body);
    } else {
        println!("post failed:", r.err);
    }

    free(c as *void);
    return ok(0);
}

Controle total com HttpClientRequest

Quando os métodos de conveniência não cobrem seu caso — um verbo arbitrário como PATCH, vários cabeçalhos personalizados, ou um corpo que você quer anexar explicitamente — construa um HttpClientRequest e passe-o para do:

pub struct HttpClientRequest {
    pub method: string,
    pub url: string,
    pub headers: string,        // linhas brutas "Name: Value\r\n"
    pub body: string,
}

pub fn new(method: string, url: string) -> *HttpClientRequest
pub fn set(self, name: string, value: string)   // acrescenta uma linha de cabeçalho
pub fn body_str(self, s: string)                // define o corpo + Content-Length

new inicia um envelope com cabeçalhos e corpo vazios. set acrescenta uma linha de cabeçalho (sem deduplicação — chame uma vez por cabeçalho). body_str armazena o payload e define o Content-Length correspondente por você, então chame-o depois do content-type:

import stdlib::http::*;
import stdlib::http::client::*;

fn main() -> !i32 {
    // Controle total: monta o envelope, define cabeçalhos, escolhe o método.
    let req: *HttpClientRequest = HttpClientRequest::new("PATCH",
                                                         "http://api.local/me");
    req.set("Authorization", "Bearer xyz");
    req.set("Accept", "application/json");
    req.set("Content-Type", "application/json");
    req.body_str("{\"name\":\"new\"}");

    let c: *HttpClient = HttpClient::new();
    let r: !*HttpResponse = c.do(req);
    if r.ok {
        println!("status:", r.val.status);
    }

    free(req as *void);
    free(c as *void);
    return ok(0);
}

do segue redirecionamentos até max_redirects saltos: um 303 rebaixa o método para GET e descarta o corpo, 301/302 fazem o mesmo para métodos que não sejam GET/HEAD, e 307/308 preservam tanto o método quanto o corpo. Valores relativos de Location são resolvidos em relação à URL atual. Quando você precisa ver a resposta 3xx intocada — ao escrever um proxy ou um crawler — use do_once, que envia exatamente uma requisição e nunca segue redirecionamentos:

import stdlib::http::*;
import stdlib::http::client::*;

fn main() -> !i32 {
    let c: *HttpClient = HttpClient::new();

    // get / post / delete / put retornam todos !*HttpResponse.
    let g: !*HttpResponse = c.get("http://api.local/items/42");
    let p: !*HttpResponse = c.post("http://api.local/upload",
                                   "raw=text",
                                   "application/x-www-form-urlencoded");
    let d: !*HttpResponse = c.delete("http://api.local/items/42");

    if g.ok { println!("get:", g.val.status); }
    if p.ok { println!("post:", p.val.status); }
    if d.ok { println!("delete:", d.val.status); }

    // do_once nunca segue redirecionamentos — vê o 30x exatamente como enviado.
    let req: *HttpClientRequest = HttpClientRequest::new("GET",
                                                         "http://api.local/old");
    let raw: !*HttpResponse = c.do_once(req);
    if raw.ok {
        let resp: *HttpResponse = raw.val;
        if resp.status == 301 || resp.status == 302 {
            println!("redirect to:", resp.header("Location"));
        }
    }

    free(req as *void);
    free(c as *void);
    return ok(0);
}

Lendo a resposta

Qualquer que seja o ponto de entrada usado, o sucesso entrega um *HttpResponse em .val. Os campos e métodos que importam no lado cliente são os mesmos que o builder do servidor produz — status, body, header(name) e headers(name) para cabeçalhos repetidos como Set-Cookie. header converte para minúsculas e remove espaços por você, então resp.header("Content-Type") e resp.header("content-type") são equivalentes, e um cabeçalho ausente retorna "" em vez de um erro.

HTTP/2, HTTP/3, JWT, proxy reverso e códigos de status

A superfície HTTP do dia a dia — roteadores, extractors, JSON — se apoia em um pequeno conjunto de blocos de construção de nível mais baixo. Esta seção é um tour guiado por essa superfície avançada: os transportes HTTP/2 e HTTP/3, verificação JWT, um proxy reverso pronto para uso, e os helpers de código de status. Cada um vive em seu próprio módulo; importe apenas o que for usar.

Códigos de status

stdlib::http_status é Glide puro — sem runtime, sem libc. Ele converte códigos numéricos em frases de razão e os classifica nas quatro faixas em que você mais ramifica. As assinaturas são:

pub fn status_text(code: i32) -> string   // "OK", "Not Found", ... "Unknown"
pub fn is_ok(code: i32) -> bool            // 200..=299
pub fn is_redirect(code: i32) -> bool      // 300..=399
pub fn is_client_error(code: i32) -> bool  // 400..=499
pub fn is_server_error(code: i32) -> bool  // 500..=599

status_text conhece o registro IANA padrão (1xx a 5xx) e retorna "Unknown" para códigos não atribuídos como 599. Os quatro predicados is_* são simples verificações de intervalo semi-aberto, o que os torna ideais para decisões de roteamento.

Helper Faixa Exemplos de códigos
is_ok 200–299 200 OK, 201 Created, 204 No Content
is_redirect 300–399 301 Moved Permanently, 304 Not Modified
is_client_error 400–499 400 Bad Request, 404 Not Found, 429 Too Many Requests
is_server_error 500–599 500 Internal Server Error, 503 Service Unavailable

Como match no Glide só corresponde a variantes de enum (nunca a literais inteiros), classifique um código com os predicados e uma escada if/else:

import stdlib::http_status::*;

fn classify(code: i32) -> string {
    if is_ok(code) { return "success"; }
    if is_redirect(code) { return "redirect"; }
    if is_client_error(code) { return "client error"; }
    if is_server_error(code) { return "server error"; }
    return "informational";
}

fn main() -> i32 {
    let codes: *Vector<i32> = Vector::new();
    codes.push(200);
    codes.push(301);
    codes.push(404);
    codes.push(503);
    for let i: i32 = 0; i < codes.len(); i++ {
        let c: i32 = codes.get(i);
        println!(format!("{} {}", c, status_text(c)), "->", classify(c));
    }
    return 0;
}

Verificação JWT

stdlib::http::jwt verifica JSON Web Tokens HS256 (HMAC-SHA256). O módulo expõe um único ponto de entrada e uma struct de claims:

pub struct JwtClaims {
    pub sub: string,
    pub exp: i32,
    pub iat: i32,
    pub raw: *JsonValue,
}

pub fn jwt_verify(token: string, key: string) -> !JwtClaims

jwt_verify divide o token compacto em suas três partes separadas por pontos, decodifica o payload em base64url, recalcula o HMAC sobre header.payload com sua key compartilhada e faz uma verificação constante contra a assinatura fornecida. Em caso de sucesso, você recebe os claims decodificados; raw é o payload completo como *JsonValue para que você possa ler claims personalizados além de sub/exp/iat.

A string de erro é prefixada com o status HTTP que você retornaria para ela, o que facilita traduzir uma rejeição em uma resposta:

Erro Significado
400:malformed jwt Não tem três partes, base64 inválido, ou o payload não é um objeto JSON com sub string
401:bad signature HMAC inválido — chave errada ou token adulterado
401:expired O claim exp está definido e já é passado
import stdlib::http::jwt::*;

fn main() -> i32 {
    let token: string = "eyJ.eyJ.sig";
    let r: !JwtClaims = jwt_verify(token, "my-secret-key");
    if r.ok {
        let claims: JwtClaims = r.val;
        println!("subject:", claims.sub);
        println!(format!("exp={} iat={}", claims.exp, claims.iat));
    } else {
        println!("rejected:", r.err);
    }
    return 0;
}

Proxy reverso

stdlib::http::proxy é um proxy reverso pronto para uso: encaminha um HttpRequest recebido para uma origem upstream e converte a resposta do upstream de volta no HttpResponse do seu servidor. A configuração e o ponto de entrada são:

pub struct ReverseProxyConfig {
    pub upstream: string,
    pub strip_prefix: string,
    pub preserve_host: bool,
    pub rewrite_location: bool,
    pub public_base: string,
    pub timeout_ms: i32,
    pub tls_insecure: bool,
}

impl ReverseProxyConfig {
    pub fn to(upstream: string) -> ReverseProxyConfig  // padrões conservadores
}

pub fn reverse_proxy(cfg: *ReverseProxyConfig, req: *HttpRequest) -> HttpResponse

ReverseProxyConfig::to(upstream) fornece uma configuração com timeout de 30 s e verificação TLS ativada. Ajuste os campos conforme necessário: strip_prefix monta uma aplicação sob um caminho local (/api/users no upstream vira /users), preserve_host mantém o cabeçalho Host do cliente, e rewrite_location + public_base reescrevem redirecionamentos Location do upstream para apontar de volta para sua origem pública. Falhas de rede ou de parse no upstream viram 502 Bad Gateway.

import stdlib::http::*;
import stdlib::http::proxy::*;

fn gateway(req: *HttpRequest) -> HttpResponse {
    let mut cfg: ReverseProxyConfig = ReverseProxyConfig::to("http://127.0.0.1:9000");
    cfg.strip_prefix = "/api";
    cfg.rewrite_location = true;
    cfg.public_base = "https://edge.example.com";
    return reverse_proxy(&cfg, req);
}

fn main() -> !i32 {
    http_listen(8080, gateway)?;
    return ok(0);
}

Para pipelines personalizados, o módulo também expõe as peças de nível mais baixo das quais reverse_proxy é construído — proxy_build_upstream_request, proxy_target_url, proxy_filter_request_headers, proxy_filter_response_headers e proxy_rewrite_location — para que você possa inspecionar ou ajustar a requisição upstream antes de ela ser enviada.

HTTP/2

stdlib::http::h2 implementa a camada de framing HTTP/2 (frames, HPACK, SETTINGS, controle de fluxo) sobre um stream TLS. HTTP/2 é quase sempre negociado sobre TLS via ALPN — você pede h2 no handshake TLS e o par concorda. As duas extremidades do módulo são uma conexão cliente e um loop servidor:

impl H2Conn {
    pub fn over(tls: *TlsStream) -> !*H2Conn        // envia preface + SETTINGS
    pub fn request(self: *H2Conn, method: string, path: string,
                   authority: string, scheme: string,
                   extra_headers: *Vector<*HpackHeader>,
                   body: string) -> !*H2Response
    pub fn close(self: *H2Conn)
}

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

pub fn h2_serve(s: *TlsStream, handler: fn(*HttpRequest) -> HttpResponse)

No lado cliente: conecte com alpn = "h2,http/1.1", verifique stream.alpn() == "h2", depois envolva o stream em um H2Conn e emita requisições. H2Conn::over escreve o preface de conexão e seu frame SETTINGS; request envia um frame HEADERS (+ DATA opcional) e lê de volta a resposta completa.

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

fn main() -> i32 {
    let cfg: *TlsConfig = TlsConfig::client();
    cfg.alpn = "h2,http/1.1";
    defer cfg.free();

    let ts: !*TlsStream = TlsStream::connect("nghttp2.org", 443, cfg);
    if !ts.ok { println!("tls:", ts.err); return 1; }
    let stream: *TlsStream = ts.val;

    if !stream.alpn().eq("h2") {
        println!("server did not negotiate h2");
        stream.close();
        return 1;
    }

    let hc: !*H2Conn = H2Conn::over(stream);
    if !hc.ok { stream.close(); return 1; }
    let h2: *H2Conn = hc.val;
    defer h2.close();

    let extras: *Vector<*HpackHeader> = Vector::new();
    let r: !*H2Response = h2.request("GET", "/", "nghttp2.org", "https", extras, "");
    if r.ok {
        println!("status:", r.val.status);
        println!("body bytes:", r.val.body.len());
    }
    return 0;
}

No lado servidor, h2_serve(stream, handler) conduz uma conexão h2-ALPN já handshakeada: lê o preface do cliente, troca SETTINGS e despacha cada requisição completamente recebida para o seu handler fn(*HttpRequest) -> HttpResponse — o mesmo formato de handler que o resto do módulo HTTP usa. Cabeçalhos extras da requisição viajam como *Vector<*HpackHeader>, onde HpackHeader é simplesmente { name, value }.

HTTP/3

stdlib::http::h3 executa HTTP/3 sobre QUIC (UDP), apoiado por ngtcp2 + nghttp3. O ponto de entrada do servidor espelha https_listen — mesmo layout de cert/chave PEM, mesmo formato de handler:

pub fn http3_listen(port: i32, cert_path: string, key_path: string,
                    handler: fn(*HttpRequest) -> HttpResponse) -> !i32

pub fn h3_make_self_signed_cert(cert_path: string, key_path: string,
                                cn: string, days: i32) -> !i32

http3_listen faz o bind da porta UDP e fica em loop aceitando conexões para sempre; uma única thread C leitora possui o socket e demultiplexa até 64 conexões simultâneas, despachando cada uma para seu handler em sua própria thread. Retorna !i32 para que uma falha de bind, certificado ou chave apareça como err. Use h3_make_self_signed_cert para gerar um certificado descartável para testes locais.

import stdlib::http::*;
import stdlib::http::h3::*;

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

fn main() -> !i32 {
    h3_make_self_signed_cert("cert.pem", "key.pem", "localhost", 365)?;
    http3_listen(443, "cert.pem", "key.pem", root)?;
    return ok(0);
}

O módulo também inclui um cliente HTTP/3 — http3_get(url, tls_insecure) e o mais completo http3_request(method, url, extra_headers, ...) — além de um H3SessionCache para persistir tickets de sessão QUIC entre execuções e habilitar retomada mais rápida.

Juntando tudo: uma API JSON completa

Você conheceu o router, os extractors tipados, Json<T>, middleware e @listen uma peça de cada vez. Esta seção os conecta em um único programa que você pode realmente executar: uma pequena API JSON no estilo CRUD para um recurso notes. Ela lista notas, busca uma pelo id e cria novas — com os códigos de status corretos (200/201/404/422), estado em memória compartilhado e um middleware de logging. O programa todo compila do início ao fim.

O programa completo

import stdlib::http::*;
import stdlib::http::router::*;
import stdlib::http::route::*;
import stdlib::http::typed::*;
import stdlib::http::extract::*;
import stdlib::json::*;
import stdlib::http_status::*;

// ---- o recurso ------------------------------------------------------

pub struct Note {
    pub id:    i32,
    pub title: string,
    pub done:  bool,
}

impl JsonBind for Note {
    fn from_json(v: *JsonValue) -> !Note {
        let title: string = v.get_string("title")?;
        let done: bool = v.opt_bool("done") ?? false;
        return ok(Note { id: 0, title: title, done: done });
    }
    fn to_json(self: *Note) -> *JsonValue {
        let o: *JsonValue = JsonValue::object();
        o.obj_set("id", JsonValue::int(self.id));
        o.obj_set("title", JsonValue::string(self.title));
        o.obj_set("done", JsonValue::bool(self.done));
        return o;
    }
}

// ---- o store em memória, compartilhado via State<Store> ----------------------

pub struct Store {
    pub notes:   *Vector<Note>,
    pub next_id: i32,
}

impl Store {
    pub fn new() -> *Store {
        let s: *Store = malloc(sizeof(Store)) as *Store;
        s.notes = Vector::new();
        s.next_id = 1;
        return s;
    }

    pub fn add(self: *Store, title: string, done: bool) -> Note {
        let n: Note = Note { id: self.next_id, title: title, done: done };
        self.notes.push(n);
        self.next_id = self.next_id + 1;
        return n;
    }

    pub fn find(self: *Store, id: i32) -> ?Note {
        for let i: i32 = 0; i < self.notes.len(); i++ {
            let n: Note = self.notes.get(i);
            if n.id == id { return some(n); }
        }
        return none();
    }
}

// ---- handlers ----------------------------------------------------------

// GET /notes -> 200 com a lista completa como array JSON.
@get("/notes")
fn list_notes(state: State<Store>) -> Json<Vector<Note>> {
    return Json::wrap(*state.val.notes);
}

// GET /notes/:id -> 200 com a nota, ou 404 quando não encontrada.
@get("/notes/:id")
fn get_note(state: State<Store>, id: Path<i32>) -> HttpResponse {
    let found: ?Note = state.val.find(id.val);
    if !found.has {
        return HttpResponse::with_status(404).text("note not found");
    }
    let n: Note = found.val;
    return HttpResponse::ok().json_of(n.to_json());
}

// POST /notes -> 201 com a nota criada. O corpo é vinculado via
// Json<Note> (400 em JSON inválido, 422 quando from_json rejeita).
@post("/notes")
fn create_note(state: State<Store>, body: Json<Note>) -> HttpResponse {
    let created: Note = state.val.add(body.val.title, body.val.done);
    return HttpResponse::with_status(201).json_of(created.to_json());
}

// ---- middleware --------------------------------------------------------

// Registra método + caminho antes do handler executar e o status depois.
fn logger(req: *HttpRequest, c: *Chain) -> HttpResponse {
    println!("->", req.method, req.path_only());
    let resp: HttpResponse = chain_next(c);
    println!("  <-", resp.status);
    return resp;
}

// ---- conecta tudo --------------------------------------------------------

@listen(8080)
fn main(r: *Router) {
    let store: *Store = Store::new();
    store.add("buy milk", false);
    store.add("write docs", true);
    r.state(store as *void);
    r.use_mw(logger);
}

O recurso e seu binding JSON

Note é uma struct simples. Para movê-la de e para a rede como JSON, ela implementa JsonBind — o mesmo trait do qual a camada tipada depende. from_json analisa um *JsonValue recebido em um Note: get_string("title") retorna !string (um campo obrigatório — propagado com ?), e opt_bool("done") retorna ?bool (opcional — padrão com ?? false). Como from_json retorna !Note, um title ausente faz o binding inteiro falhar, e o handler @post converte essa falha em um 422. to_json constrói o valor de resposta com JsonValue::object() e obj_set.

Estado compartilhado com State<T>

Handlers são ponteiros simples fn(*HttpRequest) -> HttpResponse — não há closure para capturar um banco de dados. O extractor State<T> resolve isso com um único slot de processo inteiro. Você registra o ponteiro uma vez na inicialização com Router::state; todo handler que declara um parâmetro state: State<Store> então o recebe — a macro @get/@post emite State::extract(req) por você e acessa state.val como *Store. O store é alocado com malloc(sizeof(Store)) as *Store para que seja um ponteiro bruto que sobreviva ao corpo do main — exatamente o que você quer para um estado que dure toda a vida do servidor.

Os três handlers e seus códigos de status

Cada handler é anotado com um atributo de rota (@get, @post). A macro renomeia sua função, gera um wrapper fn(*HttpRequest) -> HttpResponse que extrai todos os parâmetros tipados e registra a rota — então você nunca escreve r.get(...) manualmente.

Handler Rota Sucesso Falha
list_notes @get("/notes") 200 + array JSON
get_note @get("/notes/:id") 200 + a nota 404 quando não encontrada; 400 se :id não for int
create_note @post("/notes") 201 + nota criada 400 JSON inválido, 422 binding rejeitado

list_notes retorna Json<Vector<Note>>. A macro converte automaticamente um retorno Json<T> em uma resposta 200 via IntoResponse, chamando to_json em cada elemento (o JsonBind for Vector<T> genérico que você conheceu na seção de JSON). Note que Json::wrap(*state.val.notes) desreferencia o *Vector<Note> armazenado para envolvê-lo por valor. get_note declara id: Path<i32>, então um id não numérico nunca chega ao seu código (o extractor retorna 400); quando a busca falha, você retorna um 404 explícito. create_note declara body: Json<Note>, e a macro emite a cerimônia de binding — 400 em JSON malformado, 422 em from_json com falha.

Middleware de logging e servindo tudo

logger é um middleware comum (fn(*HttpRequest, *Chain) -> HttpResponse): ele registra a requisição, chama chain_next(c) para executar o resto da cadeia, depois registra o status da resposta e o retorna. Registrado globalmente em main com r.use_mw(logger), ele envolve toda rota. (Se você o quisesse apenas em rotas específicas, empilharia um atributo @middleware(logger) junto ao atributo de rota.)

@listen(8080) reescreve main no bootstrap do servidor. Ele cria o Router, executa o corpo do seu main com r no escopo, registra todo handler da família @route no arquivo e termina com r.listen(8080). Como você escreveu fn main(r: *Router), seu corpo configura esse mesmo router — semeando o store e instalando o middleware antes de o servidor começar a aceitar conexões.

Execute e teste a API:

shell
# listar (semeado com duas notas)
curl localhost:8080/notes
# -> [{"id":1,"title":"buy milk","done":false},{"id":2,"title":"write docs","done":true}]

# buscar uma
curl localhost:8080/notes/1            # 200 {"id":1,...}
curl -i localhost:8080/notes/99        # 404 note not found

# criar uma
curl -i -X POST localhost:8080/notes -d '{"title":"ship it"}'   # 201 {"id":3,...}
curl -i -X POST localhost:8080/notes -d '{"done":true}'         # 422 (sem title)
curl -i -X POST localhost:8080/notes -d 'not json'             # 400

Esse é o ciclo completo: um recurso tipado, estado compartilhado, extração tipada, códigos de status corretos, middleware e um servidor de uma linha. Tudo além deste ponto — autenticação, query params, SSE, arquivos estáticos — se encaixa no mesmo router da mesma forma.

Recapitulação

O que vem a seguir

Um serviço em execução só é confiável se você consegue provar que funciona. O próximo capítulo — Testes e benchmarks — cobre glide test, as macros de asserção e como medir desempenho com glide bench (incluindo o padrão routes!(r) + r.dispatch(&req) para testar handlers sem nenhum socket envolvido).