http
cliente e servidor HTTP
use httpHTTP sobre a camada 2 do
net(sockets de bytes + TLS, §4/net.md), parseando o protocolo ele mesmo. Não usa oChannel[T]tipado: aquele é o RPC nativo da linguagem (os dois lados compartilhamTem compilação), e HTTP parte da premissa oposta, falar com o mundo-não-seu, cujo contrato é o formato-de-fio (método, headers, status, bytes), agnóstico de linguagem. Não háTpra pôr no channel, então HTTP mora nos bytes. A superfície da aplicação é agnóstica de versão (Request/Response/Headers/ Body idênticos em /1.1, /2, /3); a versão vive na conexão, negociada por ALPN. Este pacote entrega os primitivos (tipos + cliente + transporte de servidor + a forma de handlerfn(Request) -> Response); router, middleware e baterias de aplicação ficam na lib oficialweb(tier 2), por cima desta.
Superfície agnóstica de versão
Seção intitulada “Superfície agnóstica de versão”A parte que não muda entre /1.1, /2 e /3. Método, status, headers, corpo, URL, e os dois envelopes (Request/Response).
Method e Status
Seção intitulada “Method e Status”decl Method { Get; Head; Post; Put; Patch; Delete; Connect; Options; Trace }fn (m: Method) to_string() -> string // Display ("GET", "POST", …)fn (Method) parse(s: string) -> Optional[Method] // none se desconhecido
decl Status { pub code: u16 } // 100..599fn (Status) of(code: u16) -> Statusfn (s: Status) reason() -> string // texto canônico ("Not Found")fn (s: Status) is_informational() -> bool // 1xxfn (s: Status) is_success() -> bool // 2xxfn (s: Status) is_redirect() -> bool // 3xxfn (s: Status) is_client_error() -> bool // 4xxfn (s: Status) is_server_error() -> bool // 5xxfn (s: Status) to_string() -> string // Display ("404 Not Found")Constantes nomeadas dos status comuns (evita números mágicos no call site):
const ok: Status // 200const created: Status // 201const accepted: Status // 202const no_content: Status // 204const moved_permanently: Status // 301const found: Status // 302const see_other: Status // 303const not_modified: Status // 304const temporary_redirect: Status // 307const permanent_redirect: Status // 308const bad_request: Status // 400const unauthorized: Status // 401const forbidden: Status // 403const not_found: Status // 404const method_not_allowed: Status // 405const conflict: Status // 409const too_many_requests: Status // 429const internal_error: Status // 500const not_implemented: Status // 501const bad_gateway: Status // 502const service_unavailable: Status // 503Headers: multi-valor, nome case-insensitive
Seção intitulada “Headers: multi-valor, nome case-insensitive”Map[string,string] não serve: header HTTP tem nome case-insensitive e multi-valor (vários
Set-Cookie, vários Accept). Daí o tipo dedicado:
decl Headers { ... }fn (Headers) new() -> Headers
fn (h: *Headers) set(name: string, value: string) // substitui TODOS os valores do nomefn (h: *Headers) add(name: string, value: string) // acrescenta (preserva os anteriores)fn (h: *Headers) remove(name: string)
fn (h: Headers) get(name: string) -> Optional[string] // 1º valor (lookup case-insensitive)fn (h: Headers) get_all(name: string) -> List[string] // todos os valores do nomefn (h: Headers) has(name: string) -> boolfn (h: Headers) names() -> Iterator[string] // nomes presentes (forma canônica)fn (h: Headers) iter() -> Iterator[(string, string)] // cada par (nome, valor)fn (h: Headers) len() -> usizeLookup é case-insensitive (h.get("content-type") acha Content-Type); a saída no fio usa a
capitalização canônica do header.
Url e Query
Seção intitulada “Url e Query”URL parseada (o cliente é o consumidor; se outro domínio precisar, promove-se pra net depois):
decl Url { pub scheme: string // "http" / "https" pub host: string pub port: Optional[u16] // none → default do scheme (80/443) pub path: string // "/" quando vazio pub query: Query pub fragment: Optional[string]}fn (Url) parse(s: string) -> Result[Url, error{Malformed}]fn (u: Url) to_string() -> string // Display (re-encoda percent-encoding)fn (u: Url) with_path(path: string) -> Urlfn (u: Url) with_query(q: Query) -> Url
decl Query { ... } // multi-valor e ORDENADO (a ordem do fio importa)fn (Query) new() -> Queryfn (Query) parse(s: string) -> Query // "a=1&b=2" → Query (decoda percent-encoding)fn (q: *Query) add(key: string, value: string)fn (q: *Query) set(key: string, value: string)fn (q: Query) get(key: string) -> Optional[string]fn (q: Query) get_all(key: string) -> List[string]fn (q: Query) iter() -> Iterator[(string, string)]fn (q: Query) to_string() -> string // "a=1&b=2" (encoda)Body: streaming por default
Seção intitulada “Body: streaming por default”O corpo satisfaz Readable + Closeable (io): o default é fluxo (não carrega tudo na
memória, o que serve download grande e SSE), com conveniências eager por cima.
decl Body { ... } // Readable + Closeablefn (Body) empty() -> Bodyfn (Body) from_bytes(data: []byte) -> Body // corpo conhecido (Content-Length)fn (Body) from_string(s: string) -> Bodyfn (Body) from_reader(r: Readable) -> Body // fluxo de tamanho desconhecido → chunked
fn (b: *Body) read(buf: mut []byte) -> Result[usize, error{Io, Closed}] // Readable (Ok(0)=fim)fn (b: *Body) close() -> error{Io} // Closeable
// conveniências (consomem o corpo; alocam pelo @mm do contexto):fn (b: *Body) read_all() -> Result[[]byte, error{Io, Closed}]fn (b: *Body) to_string() -> Result[string, error{Io, Closed, InvalidUtf8}] // valida UTF-8 na borda
fn (b: Body) len_hint() -> Optional[usize] // Content-Length se conhecido; none se chunkedRequest e Response
Seção intitulada “Request e Response”decl Request { pub method: Method pub url: Url pub headers: Headers pub body: Body pub version: Version // no servidor, a versão negociada; no cliente, o transporte decide}fn (Request) build(method: Method, url: Url) -> Request // forma explícitafn (Request) get(url: string) -> Result[Request, error{Malformed}] // conveniências (parseiam a URL)fn (Request) post(url: string, body: Body) -> Result[Request, error{Malformed}]fn (r: *Request) with_header(name: string, value: string) -> Request // encadeável (devolve ajustado)fn (r: *Request) with_body(body: Body) -> Request
decl Response { pub status: Status pub headers: Headers pub body: Body pub version: Version}fn (Response) of(status: Status) -> Response // resposta vazia com statusfn (Response) text(status: Status, s: string) -> Response // body de texto + Content-Type text/plainfn (Response) bytes(status: Status, data: []byte, content_type: string) -> Responsefn (Response) stream(status: Status, r: Readable, content_type: string) -> Response // corpo em fluxofn (r: *Response) with_header(name: string, value: string) -> Responsefn (r: *Response) with_body(body: Body) -> Response
decl Version { Http11; Http2; Http3 }fn (v: Version) to_string() -> string // "HTTP/1.1" / "HTTP/2" / "HTTP/3"Cliente
Seção intitulada “Cliente”Full por design (pool, redirect, cookie jar, descompressão), porque um cliente HTTP incompleto não serve a
sistemas reais. Tudo acontece dentro de send; o Client carrega o estado (pool de conexões, cookie jar).
decl Client { ... }fn (Client) new() -> Client // defaults sensatos (pool/redirect/cookies/gzip on)fn (Client) with(cfg: ClientConfig) -> Client
fn (c: *Client) send(req: Request) -> Result[Response, HttpError] // o método centralfn (c: *Client) get(url: string) -> Result[Response, HttpError] // conveniênciasfn (c: *Client) post(url: string, body: Body) -> Result[Response, HttpError]fn (c: *Client) put(url: string, body: Body) -> Result[Response, HttpError]fn (c: *Client) delete(url: string) -> Result[Response, HttpError]Configuração e o error-set fechado do cliente:
decl ClientConfig { pub timeout: Duration // por requisição, ponta a ponta (default 30s) pub max_redirects: u32 // 0 = não seguir (default 10) pub pool: PoolConfig pub cookies: bool // cookie jar automático (default true) pub auto_decompress: bool // gzip/deflate transparente na resposta (default true) pub tls: TlsConfig // de net: verify, sni, alpn, ca_bundle pub default_headers: Headers // mesclados em toda requisição (User-Agent, Accept, …) pub https_redirect_only: bool // recusa redirect que rebaixa https→http (default true)}decl PoolConfig { pub max_idle_per_host: u32 // conexões ociosas reusáveis por host (default 8) pub max_per_host: u32 // teto de conexões simultâneas por host (0 = ilimitado) pub idle_timeout: Duration // fecha a ociosa após (default 90s)}
alias HttpError = error{ Malformed, // URL ou resposta malformada Dns, // resolução de nome falhou Connect, // não conectou (recusado / inalcançável) Tls, // handshake / certificado Timeout, // estourou o timeout da requisição TooManyRedirects, // excedeu max_redirects Closed, // a conexão fechou no meio Io}use http
client := http.Client.new()resp := client.get("https://api.exemplo.com/users") catch |e| { return e }if resp.status.is_success() { body := resp.body.to_string() catch |e| { return e } process(body)}Download grande sem carregar tudo na memória: o corpo é Readable, então io.copy o bombeia.
resp := client.get("https://exemplo.com/grande.bin") catch |e| { return e }defer resp.body.close() // devolve a conexão ao poolout := fs.create("grande.bin") catch |e| { return e }io.copy(out, resp.body) catch |e| { return e } // fluxo → arquivo, em blocosCiclo da conexão e do pool: send reusa uma conexão ociosa do pool para o mesmo (scheme, host, port)
ou abre uma nova; a conexão só volta ao pool quando o corpo da resposta é drenado ou fechado, por isso
leia o corpo até o fim (read_all/to_string) ou defer resp.body.close(). Redirects são seguidos
até max_redirects, com a semântica do HTTP (303 vira GET; 307/308 preservam método e corpo); um corpo
de requisição em fluxo não é replayable, então um redirect que exija reenviá-lo vira Err (use corpo
eager, from_bytes/from_string, quando precisar sobreviver a redirect). Cookie jar e
descompressão são automáticos quando ligados.
Cookie jar (automático; exposto para inspeção/controle):
fn (c: *Client) jar() -> *CookieJardecl CookieJar { ... }fn (j: *CookieJar) add(url: Url, c: Cookie)fn (j: *CookieJar) cookies_for(url: Url) -> List[Cookie]fn (j: *CookieJar) clear()
decl Cookie { pub name: string pub value: string pub domain: string pub path: string pub expires: Optional[DateTime] // de `time`; none = sessão pub secure: bool pub http_only: bool}Servidor: um processo por stream
Seção intitulada “Servidor: um processo por stream”O handler é só uma função fn(Request) -> Response, sem tipo novo:
alias Handler = fn(Request) -> Responsefn serve(addr: string, handler: Handler) -> error{AddrInUse, PermissionDenied, Io} // sobe e bloqueiafn serve_with(cfg: ServerConfig, handler: Handler) -> error{AddrInUse, PermissionDenied, Io}
// forma controlável (shutdown gracioso):fn bind(cfg: ServerConfig, handler: Handler) -> Result[Server, error{AddrInUse, PermissionDenied}]decl Server { pub addr: Address }fn (s: *Server) run() -> error{Io} // serve até shutdown (suspende este processo)fn (s: *Server) shutdown(grace: Duration) -> error{Io} // para de aceitar; drena em 'grace'; fecha
decl ServerConfig { pub addr: string pub tls: Optional[TlsConfig] // none = texto puro (http/1.1, h2c); some = HTTPS (h2/h3 via ALPN) pub versions: List[Version] // o que oferecer (default: tudo que o transporte suporta) pub read_timeout: Duration // por requisição pub write_timeout: Duration pub max_header_bytes: usize // teto de headers (anti-abuso) pub max_body_bytes: Optional[usize] // teto de corpo (none = sem teto)}O modelo é o de atores (§3/§8) estendido à HTTP. Em /1.1, cada conexão é um stream serial, e o
servidor faz spawn de um processo rodando handler(req). Em /2 e /3, a conexão é multiplexada: um
processo dono demultiplexa os frames (HPACK/QPACK, controle de fluxo, estado de stream) e faz spawn
de um processo por stream. Em qualquer versão, o handler roda no seu próprio processo: um panic
numa requisição crasha só aquele processo (o supervisor observa pelo catch, §8), nunca o servidor, e
o cliente recebe 500. A resposta pode ter corpo em fluxo (Response.stream) para SSE e streaming, sem
materializar tudo.
use http
fn handle(req: Request) -> Response { match req.method { Get => Response.text(http.ok, "olá, {{req.url.path}}") _ => Response.of(http.method_not_allowed) }}
fn main() { http.serve("0.0.0.0:8080", handle) catch |e| { io.eprintln("falha ao subir: {{e}}") os.exit(1) }}Shutdown gracioso casa com os sinais como channel (os.signals, §4): rode o servidor num processo e
peça shutdown quando o sinal chegar.
srv := http.bind(cfg, handle) catch |e| { os.exit(1) }spawn srv.run()sigs := os.signals(.Interrupt, .Terminate)loop s in sigs { match s { Interrupt | Terminate => { srv.shutdown(10s); break } _ => {} }}Camada de transporte: onde a versão vive
Seção intitulada “Camada de transporte: onde a versão vive”A superfície acima é idêntica nas três versões; a diferença mora aqui, e cada versão assenta no net
(camada 2) de um jeito. A versão é negociada, não escolhida por chamada.
- HTTP/1.1, sobre
TcpStream(ouTlsStream), §net. Uma requisição/resposta por vez na conexão;keep-alivereusa a conexão em série; corpo de tamanho desconhecido vai chunked. O pool guarda conexões ociosas (muitas conexões por host para concorrência). É o corte completo do primeiro release. - HTTP/2, sobre um único
TlsStream(ALPN negocia"h2") ou texto puroh2c. Uma conexão multiplexa muitos streams: o processo-dono roda a camada de frames (HPACK, controle de fluxo) e fazspawnpor stream. Isso refina o modelo de atores, processo-por-stream em vez de processo-por-conexão, sem quebrá-lo; e o pool passa a manter uma conexão por host com muitos streams concorrentes (em vez das muitas conexões do /1.1). - HTTP/3, sobre QUIC (UDP, via
udp_bind/datagramas donet), com TLS 1.3 embutido no transporte, multiplex próprio (sem head-of-line blocking de TCP) e 0-RTT. ALPN"h3"+Alt-Svcanuncia a disponibilidade. A superfície da aplicação não muda; o que é grande é a implementação: uma stack QUIC (controle de congestão, recuperação de perda, multiplex sobre UDP) é um subsistema inteiro, entregue conforme a lib amadurece. O compromisso está fechado pela API já agnóstica de versão; o peso é a stack.
Como a versão é decidida. No cliente, o ALPN durante o TLS negocia a maior versão mutuamente
suportada (h2/h3 se oferecidas, senão http/1.1); h3 adicionalmente depende de descoberta por
Alt-Svc ou opt-in. No servidor, ServerConfig.versions lista o que se oferece, e o ALPN seleciona por
conexão. Não há botão de versão por requisição: a superfície é agnóstica, a conexão resolve. (Forçar
uma versão específica, p/ teste, é direção futura via ClientConfig.)
Curadoria
Seção intitulada “Curadoria”- Tier 1, sobre a camada 2 do
net. HTTP fala com o mundo-não-seu via bytes + TLS, não peloChannel[T]tipado. O channel tipado é o RPC nativo (ambos os lados compartilhamT); HTTP é agnóstico de linguagem, não háT. É diferença de categoria, não limitação, e por isso a lib parseia o protocolo ela mesma sobreTcpStream/TlsStream/QUIC. - Superfície agnóstica de versão (como o
net/httpdo Go): Request/Response/Headers/Body idênticos em /1.1, /2, /3; a versão vive na conexão, negociada por ALPN. Sem versão por chamada. - Processo por stream, não por conexão. Modelo de atores (§3/§8) estendido: /1.1 usa processo por conexão;
/2-3 usa processo por stream multiplexado.
panicnuma requisição crasha só o processo dela; o servidor segue. É a tolerância a falhas da linguagem aplicada a HTTP, de graça. - Framework é o
web(tier 2), separado.httpé primitivo (tipos + cliente + transporte de servidor + a formafn(Request) -> Response); router, middleware e baterias de app sobem noweb, e a UI (§20) sobe noweb. Quem importahttpquer construir; quem quer baterias importaweb. Foi por isso que HTTP ficou não-core: ela é, ela própria, uma pilha em camadas. - Cliente full (pool/redirect/cookie/descompressão) porque mira serviços/sistemas, onde cliente
incompleto é inútil. Tudo dentro de
Client.send; o estado (pool, jar) mora noClient. - Corpo streaming por default:
BodysatisfazReadable+Closeable;read_all/to_stringsão conveniências. Download grande e SSE sem materializar tudo. - Async colorless (§12): nenhuma função é
async; o processo suspende no IO; paralelismo éspawnpor stream. SemFuture/await/context.Context; cancelamento étimeoutou morte de processo (§8). /3comprometido, implementação adiada. A superfície já agnóstica deixa QUIC entrar sem trauma; a stack QUIC é o peso real, entregue conforme a lib amadurece.- Descompressão usa codecs de
encoding.auto_decompressapoia-se em DEFLATE/gzip (formatos frozen, candidatos aencoding/compressconforme brotli/zstd entram);httpsó os orquestra, o mesmo papel que onet/TLS faz comcrypto. A lib não reimplementa compressão. - Sem allocator nas assinaturas (colorless); buffers de streaming alocam pelo
@mmdo contexto, ou você passamut []byteonde quer controle (a portaread(buf)doReadable). Urlmora aqui (o consumidor é o cliente); promove-se pranetse outro domínio passar a precisar.