Resumo teórico
Quando você pede para um amigo passar o sal, vocês dois seguem regras escondidas: você diz por favor, ele olha para você, entrega o sal e você agradece. Se um dos dois quebrasse as regras, o sal nunca chegaria. Os computadores também precisam de regras assim. Um protocolo é um conjunto de regras que diz quem fala primeiro, como é a cara de uma mensagem e o que cada resposta significa. Como todos os programas seguem as mesmas regras, um navegador escrito em um país consegue conversar com um servidor escrito por outra pessoa em outro país.
Esta área trata dos protocolos que você usa toda vez que abre uma página, assiste a um vídeo, conversa em um chat ou chama o servidor de um aplicativo. Começamos pelo HTTP, a língua da web: seus métodos, suas respostas numeradas chamadas códigos de status e seus bilhetes chamados cabeçalhos (headers). Depois vemos como a web economiza tempo com caches, como os cookies lembram de você e como os navegadores protegem você com o CORS. Em seguida vem o TLS, o cadeado que guarda os segredos, e as três versões do HTTP (1.1, 2 e 3), que levam as mesmas mensagens de jeitos cada vez mais rápidos.
Em cima do HTTP as pessoas constroem APIs, que são cardápios de coisas que um programa pode pedir para outro programa fazer. Você vai conhecer os estilos REST, GraphQL, JSON-RPC e gRPC, e também o WebSocket e os server-sent events, que deixam o servidor falar com você sem ser perguntado primeiro. No fim você vai saber para que serve cada um e quanto cada um custa, que é exatamente o que o quiz pergunta.
O panorama: conversar pela rede
Pense em um restaurante. Você é o cliente (client): pede coisas. O garçom leva o pedido à cozinha, que é o servidor (server): ela prepara e manda de volta. Na web é igual. O navegador envia uma requisição (request: 'me dê esta página') e o servidor envia uma resposta (response: 'aqui está'). A conversa sempre começa pelo cliente.
As mensagens não voam por mágica. Elas passam por camadas, como uma carta que vai dentro de um envelope, que vai dentro de um saco, que vai em um caminhão. Cada camada tem o seu trabalho.
seu programa 'GET /foto' <- HTTP (esta área)
|
criptografia embaralha os bytes <- TLS
|
transporte entrega os bytes <- TCP ou QUIC (UDP)
|
a rede leva pacotes <- IP, cabos, Wi-Fi| Palavra | Significado |
|---|---|
| Protocolo | Um conjunto de regras para uma conversa entre programas. |
| TCPum protocolo de transporte que entrega os bytes em ordem e reenvia os que se perdem | Confiável, mas faz você esperar pela primeira peça que faltou. |
| UDPum transporte bem simples que só envia pacotes, sem prometer que eles chegam | Rápido e simples. O QUIC é construído em cima dele. |
| Round trip (RTT)o tempo para uma mensagem ir até o outro lado e a resposta voltar | A unidade que usamos para contar como um handshake é lento. |
| Latência | O atraso da rede. Latência alta significa que cada round trip é lento. |
Semântica do HTTP: métodos, códigos de status e cabeçalhos
O HTTP (HyperText Transfer Protocol) é a língua da web. Toda mensagem HTTP tem três partes: uma primeira linha que diz o que se pede (ou como foi), depois os cabeçalhos (headers), que são pequenos bilhetes, e por fim um corpo (body) opcional, que é o conteúdo de verdade, como uma foto ou um texto.
GET /foto.png HTTP/1.1
Host: exemplo.test
Accept: image/png
- Toda linha termina com dois caracteres invisíveis, CR e LF (escritos
\r\n). Uma linha vazia significa 'os cabeçalhos acabaram'. - A primeira linha de uma resposta é a status line: a versão, um código de status de três dígitos e um texto curto.
- No HTTP/1.1 o cabeçalho
Hosté obrigatório. Um computador pode hospedar muitos sites, e oHostdiz qual site você quer. Sem ele (ou com dois), o servidor deve responder400 Bad Request.
Métodos: os verbos
O método diz o que você quer fazer. As regras oficiais (RFC 9110) descrevem três propriedades. Um método é seguro (safe) se só lê e não pede nenhuma mudança. É idempotente se fazer várias vezes tem o mesmo efeito de fazer uma vez. É cacheável se a resposta pode ser guardada para reutilização.
| Método | O que faz | Seguro | Idempotente |
|---|---|---|---|
| GET | Lê um recurso | sim | sim |
| HEAD | Igual ao GET, mas só os cabeçalhos voltam, sem corpo | sim | sim |
| OPTIONS | Pergunta o que é permitido | sim | sim |
| PUT | Substitui um recurso pelo que você envia | não | sim |
| DELETE | Remove um recurso | não | sim |
| POST | Envia algo para ser processado, muitas vezes para criar | não | não |
| PATCH | Muda parte de um recurso | não | não |
Idempotente fala do efeito, não da resposta. O primeiro DELETE pode responder 204 e o segundo 404, e o método continua idempotente, porque o estado final é o mesmo: a coisa sumiu.
Códigos de status: como foi
O primeiro dígito diz a família: 1xx informação, 2xx sucesso, 3xx vá para outro lugar, 4xx o cliente errou, 5xx o servidor errou.
| Código | Significado | Quando |
|---|---|---|
| 200 / 201 / 204 | OK / Created / No Content | Created vem com um cabeçalho Location com a nova URL |
| 206 | Partial Content | Resposta a uma requisição Range (retomar um download) |
| 301, 302, 303, 307, 308 | Redirecionamentos | 301 e 308 são permanentes; 302 e 307, temporários. 307 e 308 mantêm o método e o corpo; 303 sempre quer dizer 'agora faça um GET em outra página' |
| 304 | Not Modified | Sua cópia guardada ainda é boa |
| 400 | Bad Request | A mensagem está quebrada (linha errada, falta Host) |
| 401 | Unauthorized | Na verdade quer dizer 'não autenticado': diga quem você é. Vem com WWW-Authenticate |
| 403 | Forbidden | Sei quem você é, e a resposta é não |
| 404 / 405 | Not Found / Method Not Allowed | Não existe esse caminho / o caminho existe, mas não para esse método |
| 406 / 415 | Not Acceptable / Unsupported Media Type | O servidor não consegue dar o formato do Accept / o servidor não aceita o Content-Type que você enviou |
| 409 / 412 | Conflict / Precondition Failed | Bate de frente com o estado atual / uma condição If-Match era falsa |
| 413 / 414 / 431 | Grande demais | O corpo / a URL / os cabeçalhos são grandes demais |
| 422 | Unprocessable Content | Formato certo, mas o conteúdo quebra uma regra |
| 500 / 501 / 505 | Erro do servidor | Bug / não implementado / versão do HTTP não suportada |
Onde o corpo termina?
No HTTP/1.1 a conexão fica aberta (isso se chama keep-alive e é o padrão: abrir uma conexão custa um round trip, então a reaproveitamos). A mensagem seguinte vem logo depois da primeira, então quem lê precisa saber exatamente onde cada corpo para. Há duas maneiras.
| Pergunta | Content-Length | Transfer-Encoding: chunked |
|---|---|---|
| Quando usar | O tamanho é conhecido antes de enviar | O tamanho ainda não é conhecido (um fluxo) |
| Como termina | Ler exatamente essa quantidade de bytes | Chunks, cada um com seu tamanho em hexadecimalcontar na base 16, com os dígitos de 0 a 9 e de a a f; então b vale 11; um chunk de tamanho 0 encerra |
| No fio | hello world | b\r\nhello world\r\n0\r\n\r\n |
Se uma mensagem tem os dois cabeçalhos, o Transfer-Encoding vence e o servidor deve de preferência recusá-la com 400. Por quê? Se um proxy acredita em um e o servidor atrás dele acredita no outro, os dois discordam sobre onde a requisição termina, e os bytes que sobram são lidos como uma nova requisição. Esse ataque se chama request smuggling. Fechar a conexão no fim do corpo também funciona, mas aí ela não pode ser reaproveitada.
Cabeçalhos úteis
Content-Typedescreve o corpo que você está enviando;Acceptdiz quais formatos você está disposto a receber.ETagé uma etiqueta (uma impressão digital) da versão atual de um recurso. Mandá-la de volta emIf-Matchem um PUT quer dizer 'só escreva se ninguém mudou desde que eu li'. Se alguém mudou, o servidor responde412. Isso evita a atualização perdida (lost update), em que duas pessoas editam e a segunda apaga o trabalho da primeira.Range: bytes=1000-pede o resto de um arquivo.If-Range: "a"acrescenta: 'só se a versão ainda for"a"'. Se for, você recebe206com a parte; se o arquivo mudou, recebe200com o arquivo novo inteiro.
TLS: o cadeado da conexão
O HTTP normal viaja em texto puro. Qualquer um no caminho (o dono de um Wi-Fi, um roteador) poderia ler ou mudar a mensagem. O TLS (Transport Layer Security) envolve a conversa em um cadeado. O HTTP dentro do TLS é o HTTPS. Antes de qualquer dado da web andar, os dois lados fazem uma dança curta chamada handshake.
Um handshake concluído dá três coisas: os dois combinam uma versão e os métodos de embaralhar segredos, o cliente autentica o servidor (confere se ele é mesmo quem diz ser) e os dois lados ficam com a mesma chave secreta, que mais ninguém conhece. Fazer o cliente provar quem é com um certificado também existe, mas na web é opcional e raro.
Dois jeitos de guardar segredo
| Tipo | Assimétrica | Simétrica |
|---|---|---|
| Ideia | Uma chave pública e uma chave privada que andam juntas | Uma chave só, compartilhada pelos dois lados |
| Velocidade | Lenta e cara | Muito rápida |
| Usada no TLS para | Assinar (provar identidade) e Diffie-Hellmanum truque de matemática esperto que deixa dois lados combinarem um número secreto conversando em público (combinar um segredo) durante o handshake | Proteger todos os dados depois, com um modo AEAD que dá sigilo e detecção de adulteração ao mesmo tempo |
Como o cliente confia no servidor
O servidor mostra um certificado: uma carteira de identidade com o nome e a chave pública dele, assinada por uma autoridade certificadora (CA). Normalmente a cadeia tem três elos: uma CA raiz assina uma intermediária, que assina o certificado do site. O seu sistema operacional ou navegador já vem com uma lista de raízes confiáveis. O cliente confere as assinaturas subindo a cadeia, se as datas valem e se o nome no certificado bate com o host que ele queria.
Extensões úteis na primeira mensagem (ClientHello)
- SNI (Server Name Indication): o cliente escreve o nome do host na primeira mensagem. O servidor precisa dele para escolher o certificado certo antes de ler qualquer HTTP, então não pode esperar pelo cabeçalho
Host. É o virtual hosting, uma camada abaixo. - ALPN: o cliente lista os protocolos de aplicação que fala (
h2,http/1.1) e o servidor escolhe um. Não custa nenhum round trip extra. Se o servidor não conhece o ALPN, a conexão simplesmente continua em HTTP/1.1.
Quantos round trips?
Aqui contamos depois do handshake do TCP, sem retomada de sessão. O TLS 1.2 precisa de 2 round trips. O TLS 1.3 precisa de só 1: o cliente adivinha o método de chaves e já envia a sua parte (key_share) na primeira mensagem, então o servidor consegue responder tudo de uma vez.
TLS 1.3 TLS 1.2
cliente servidor cliente servidor
|-- ClientHello + chave->| |-- ClientHello ------->|
|<-- ServerHello, cert, | |<-- ServerHello, cert -| round trip 1
| Finished -----------| |-- troca de chaves --->|
|-- Finished + dados --->| |<-- Finished ----------| round trip 2
| (1 round trip) | |-- dados ------------->|Forward secrecy e 0-RTT
Imagine que um espião grava a sua conversa criptografada durante meses e depois rouba a chave privada do servidor. Com forward secrecy (sigilo futuro) as conversas antigas continuam seguras, porque cada conexão usou um par Diffie-Hellman novo e descartável, apagado depois, e a chave do certificado só assina. O TLS 1.2 antigo com troca de chaves RSA estática não tinha isso: chave roubada, todas as gravações legíveis. O TLS 1.3 removeu o RSA estático e tornou o forward secrecy obrigatório.
Quando você reconecta, o TLS 1.3 pode usar um segredo guardado (um PSK, vindo de um ticket) e enviar dados já no primeiro voo. É o 0-RTT, ou early data. É mais rápido, mas o servidor não consegue acrescentar um valor novo, então um atacante pode repetir (replay) esse primeiro voo. O early data também não tem forward secrecy. Por isso só deve levar requisições que são seguras de repetir.
↑ Voltar ao sumárioHTTP/2: muitas requisições em uma conexão
O HTTP/1.1 tem um problema de fila: uma conexão leva uma requisição por vez. Para andar mais rápido, os navegadores abrem cerca de seis conexões por origem. Uma página com 200 imagens pequenas precisa então de 34 rodadas de seis, e cada rodada custa um round trip. Com 50 ms de atraso isso soma segundos.
Frames, streams e mensagens
O HTTP/2 mantém o mesmo significado (métodos, códigos de status, cabeçalhos) e muda só como tudo é enviado. Ele é binário, não texto. A menor peça é o frame (quadro), com um cabeçalho fixo de 9 bytes que tem tamanho, tipo, flags e um número de stream. Um stream é um canal lógico de duas vias dentro da conexão. Uma mensagem (uma requisição ou uma resposta) é o grupo de frames de um stream. Frames de streams diferentes são misturados, o que se chama multiplexação.
uma conexão TCP:
[S1 HEADERS][S3 HEADERS][S1 DATA][S5 HEADERS][S3 DATA][S1 DATA]...
^ stream 1 ^ stream 3 ... frames de vários streams intercalados- Os números de stream têm 31 bits. O cliente usa números ímpares, o servidor usa pares, e o 0 é para a própria conexão. Eles só crescem e nunca são reutilizados.
- O HTTP/2 não tem request line nem status line. Elas viram pseudo-cabeçalhos que começam com dois-pontos:
:method,:scheme,:authority(substitui oHost),:pathe:statusna resposta. - Tipos de frame:
HEADERSeDATAlevam a mensagem.RST_STREAMcancela um stream (no HTTP/1.1 era preciso fechar a conexão).GOAWAYencerra a conexão.SETTINGS,PINGeWINDOW_UPDATEcuidam de configurações, de checar se o outro lado está vivo e do controle de fluxo. - Você começa o HTTP/2 sobre TLS usando o ALPN com o identificador
h2. Oh2cé HTTP/2 sem TLS, usado entre serviços internos, não por navegadores.
HPACK: encolher os cabeçalhos
Os cabeçalhos se repetem muito: o mesmo user-agent e o mesmo cookie vão em toda requisição. O HTTP/2 os comprime com o HPACK. Ele mantém duas tabelas. A tabela estática tem 61 entradas fixas para cabeçalhos muito comuns, como :method: GET. A tabela dinâmica começa vazia e se enche com os cabeçalhos vistos nesta conexão (os mais antigos saem quando ela enche). Os dois lados mantêm a mesma tabela, então um cabeçalho já visto custa só um pequeno número de índice.
Por que não usar simplesmente o gzip, como fazia o SPDY, mais antigo? Porque comprimir um segredo (como um cookie) junto com texto que o atacante controla vaza informação pelo tamanho. É o ataque CRIME. O HPACK nunca procura repetições parciais dentro de um valor: ele troca o cabeçalho inteiro por um índice ou usa um código de Huffmanuma tabela que escreve símbolos comuns com poucos bits e símbolos raros com muitos fixo, e pode marcar campos sensíveis como 'nunca indexar'.
Controle de fluxo
O controle de fluxo (flow control) impede que um emissor rápido afogue um receptor lento. Ele vale só para frames DATA e funciona com duas janelas: uma para cada stream e uma para a conexão inteira. As duas começam em 65.535 bytes. Cada byte enviado é subtraído de ambas. O receptor devolve crédito com WINDOW_UPDATE. O emissor pode enviar no máximo o menor dos dois saldos. (Prioridade é outra coisa: só uma dica sobre qual stream atender primeiro.)
janela da conexão: 65535
stream 1 envia 16384 + 16384 + 7232 = 40000 bytes
janela da conexão restante: 25535
janela do stream 3 (nunca usado): 65535
stream 3 pode enviar: min(65535, 25535) = 25535 bytesO que não deu certo
- Server push: o servidor enviava recursos que o cliente ainda não tinha pedido. Mas o servidor não sabe o que o cliente já tem no cache, então muitas vezes desperdiçava dados. Os navegadores o removeram. O substituto é o
103 Early Hintse orel=preload, em que o navegador decide. - Domain sharding (repartir imagens por vários nomes de host, um truque do HTTP/1.1) agora atrapalha: cada novo host custa uma consulta de DNSa lista telefônica da internet, que transforma um nome como exemplo.test em um endereço, um handshake TCP e TLS e um novo slow start, e quebra a única conexão aquecida e a única tabela HPACK.
- Head-of-line blocking no TCP: o HTTP/2 resolveu a fila na aplicação, mas todos os streams dividem um único fluxo de bytes ordenado do TCP. Se um pacote se perde, o TCP segura tudo o que veio depois, até frames de streams que não perderam nada. Em um link com perdas, uma conexão HTTP/2 pode ser pior que seis conexões HTTP/1.1.
- HTTP/1.1 (6 conexões)
- 34 rodadas
- HTTP/2 (1 conexão)
- 1 rodadas
HTTP/3 e QUIC
Para acabar com o problema da fila única do TCP, a Internet construiu um transporte novo chamado QUIC. Ele roda em cima do UDP, mas o UDP é só um envelope que atravessa a rede que já existe. Todo o resto vive dentro do QUIC: streams, reenvio de dados perdidos, controle de fluxo, controle de congestionamentodiminuir a velocidade quando a rede parece cheia, para o emissor não piorar a situação e criptografia. Como o QUIC costuma rodar em espaço de usuário (dentro do navegador ou do servidor, não no sistema operacional), ele evolui com uma atualização do aplicativo. O custo: usa mais CPU por pacote que o TCP do kernel. O HTTP/3 é o HTTP levado sobre o QUIC.
| Característica | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Transporte | TCP | TCP | QUIC sobre UDP |
| Formato | Texto | Frames binários | Frames binários |
| Requisições ao mesmo tempo em uma conexão | 1 | Muitas | Muitas |
| Compressão de cabeçalhos | nenhuma | HPACK | QPACK |
| Round trips antes da primeira requisição (TLS 1.3, primeira visita) | 2 | 2 | 1 |
| Um pacote perdido atrasa | a sua conexão | todos os streams | só o seu próprio stream |
Três grandes ganhos
- Sem head-of-line blocking no transporte. Cada stream do QUIC é ordenado por conta própria. Quando um pacote se perde, só esperam os streams cujos dados ele levava. Os outros continuam chegando.
- Um handshake mais curto. Com o TCP, primeiro vem o TCP (1 round trip) e depois o TLS 1.3 (1 round trip): 2 no total antes da primeira requisição HTTP. O QUIC leva o handshake do TLS 1.3 dentro dos seus primeiros pacotes: 1. (Contado para uma primeira visita, sem retomada e sem TCP Fast Open. Em uma conexão retomada os dois podem usar 0-RTT, com o risco de replay que você viu na seção de TLS.)
- Migração de conexão. Uma conexão TCP está amarrada a quatro coisas: dois endereços IP e duas portas. Se o seu celular passa do Wi-Fi para os dados móveis, o IP muda e o TCP se perde. O QUIC usa connection IDs levados nos pacotes, então o servidor confere o novo caminho e segue com as mesmas chaves e os mesmos streams.
TCP + TLS 1.3 QUIC
cliente servidor cliente servidor
|--SYN--------->| |--Initial+ClientHello->|
|<--SYN/ACK-----| 1 RTT |<--Initial+ServerHello-| 1 RTT
|--ACK+Hello--->| |--Finished + request-->|
|<--ServerHello-| 2 RTT |
|--Finished+GET>|
Não existe QUIC sem criptografia: o handshake do TLS 1.3 faz parte de conectar, e versões anteriores do TLS não são aceitas. O QUIC também criptografa quase toda a sua própria informação de controle (números de pacote, confirmações). O motivo é a ossificação (ossification): quando caixas no meio da Internet se acostumam a ler o formato de um protocolo, elas quebram qualquer mudança, mesmo uma válida. O TCP sofreu com isso porque o seu cabeçalho está às claras. O QUIC deixa visível só o mínimo, como o connection ID, para que a rede não dependa do resto.
QPACK, Alt-Svc e fallback
- O QPACK substitui o HPACK. A tabela do HPACK é alterada pelos próprios blocos de cabeçalhos, então eles precisam ser decodificados na ordem exata, mas o QUIC só ordena dados dentro de cada stream. O QPACK envia as atualizações da tabela em um stream separado, e cada bloco diz de quais entradas precisa, então o decodificador sabe se deve esperar.
- Descobrir que o HTTP/3 existe. Um endereço
https://não diz a versão, então o navegador começa pelo TCP. O servidor então anunciaalt-svc: h3=":443"; ma=86400(protocolo, porta UDP, por quanto tempo vale). Outro caminho é um registro DNS especial do tipo HTTPS. Aí a próxima conexão pode usar QUIC. - É só uma dica. Algumas redes bloqueiam o UDP. O navegador tenta o QUIC e, se não funcionar, usa a conexão TCP (HTTP/2 ou HTTP/1.1), geralmente disputando os dois em paralelo para o usuário não esperar um timeout.
- TCP + TLS 1.3 (HTTP/1.1 ou HTTP/2)
- 2 round trips
- QUIC (HTTP/3)
- 1 round trips
Quando o HTTP/3 é melhor? Quando a latência é alta (o handshake economiza um round trip) ou quando há perda de pacotes (os streams não se bloqueiam), como nas redes móveis. Em redes rápidas e limpas, HTTP/2 e HTTP/3 rendem mais ou menos igual, e o QUIC em espaço de usuário pode até gastar mais CPU.
↑ Voltar ao sumárioREST: uma API feita de coisas
Uma API é um cardápio de coisas que um programa pode pedir a outro programa. REST é um estilo de construir APIs web, descrito por Roy Fielding na sua tese de doutorado (2000). A ideia principal: não pense em ações, pense em coisas (chamadas recursos), como livros, pedidos ou usuários. Toda coisa tem um endereço, uma URL.
| Quero... | Requisição | Resposta |
|---|---|---|
| Ler o livro 42 | GET /books/42 | 200 OK e o livro |
| Criar um livro | POST /books | 201 Created com um cabeçalho Location com a nova URL |
| Substituir o livro 42 | PUT /books/42 (o livro inteiro) | 200 ou 204 |
| Mudar um campo | PATCH /books/42 (só as mudanças) | 200 ou 204 |
| Remover o livro 42 | DELETE /books/42 | 204 No Content |
Não coloque a ação no caminho (/getBook?id=42). Fazer isso joga fora o que navegadores, caches e proxies já sabem sobre os métodos: por exemplo, que o GET é seguro e cacheável.
Stateless (sem estado)
No REST o servidor não guarda memória da conversa com o cliente. Cada requisição leva tudo o que é preciso para ser entendida (credenciais, parâmetros), então qualquer cópia do servidor pode respondê-la. Isso ajuda a escalar com muitos servidores e a se recuperar de falhas. O custo: alguns dados se repetem em cada requisição. Isto fala do estado de sessão; o servidor continua guardando recursos como livros normalmente.
Modelo de maturidade de Richardson
- Nível 0: um único endereço usado como túnel para tudo.
- Nível 1: recursos separados, cada um com a sua URL.
- Nível 2: métodos HTTP e códigos de status de verdade.
- Nível 3: controles de hipermídia (HATEOAS): a resposta diz ao cliente o que ele pode fazer em seguida, com links.
Escolhendo o status certo
400 para uma requisição malformada, 404 quando o alvo não existe, 409 quando uma requisição válida bate de frente com o estado atual (por exemplo, um segundo livro com um ISBN que deve ser único) e 422 quando o formato está certo mas o conteúdo quebra uma regra. Códigos precisos deixam os clientes reagirem sem ler o texto do erro.
Repetições: DELETE e Idempotency-Key
Depois de um timeout o cliente não sabe se o servidor terminou o serviço. Repetir um DELETE é seguro: o segundo pode devolver 404, e isso não quebra a idempotência, porque o estado final é o mesmo. O POST não é idempotente, e repeti-lo pode criar dois pedidos. A solução é uma chave de idempotência: o cliente inventa um valor único (Idempotency-Key: abc123) e o repete em cada nova tentativa. O servidor reconhece a repetição e devolve o resultado da primeira execução. Esse cabeçalho é uma convenção comum, não faz parte do HTTP básico.
PUT ou PATCH?
O PUT substitui o estado pela representação que você envia, então você precisa mandar o recurso completo, e ele é idempotente. O PATCH envia só as mudanças, o que economiza bytes e evita sobrescrever campos que outras pessoas mudaram, mas por definição não é idempotente.
Paginação
Listas grandes são devolvidas em páginas. A paginação por offset diz 'pule 3, me dê 3'. Se dois itens novos são adicionados no topo entre duas requisições de página, a janela se desloca e você recebe itens que já tinha visto. A paginação por cursor diz 'me dê 3 itens depois do id 8', então continua a partir do último item visto e fica estável, mas você não consegue pular direto para a página 50.
mais novo primeiro: 12 11 10 9 8 7 6 5 4 3 2 1 (12 e 11 entraram depois da página 1)
a página 1 foi: 10 9 8
offset=3 : pula 12 11 10 -> devolve 9 8 7 (9 e 8 repetidos)
before=8 : id < 8 -> devolve 7 6 5 (nada repetido)Over-fetching e under-fetching
No REST quem decide o formato de cada resposta é o servidor. Se ele dá mais do que a tela precisa, bytes são desperdiçados: over-fetching. Se dá menos, o cliente precisa fazer mais requisições: under-fetching. Exemplo: uma tela com um autor, seus 5 livros e a editora de cada livro (5 diferentes) precisa de 1 + 5 + 5 = 11 requisições com endpoints que devolvem só ids. Soluções: endpoints compostos, um parâmetro expand ou o GraphQL.
GraphQL: peça exatamente o que você quer
O GraphQL tem um endereço só e um schema tipado, uma lista de todos os tipos e campos que existem. O cliente envia uma query que lista os campos exatos que quer, até atravessando relações, e a resposta tem o mesmo formato. Uma requisição pode substituir as várias requisições do REST.
query {
book(id: 7) {
title
author { name }
}
}O schema e os tipos raiz
- Todo tipo é anulável (nullable) por padrão. Um
!o torna não nulo:title: String!significa que o servidor promete nunca devolver null ali (em um argumento, o cliente é obrigado a informar um valor). - Um schema tem até três tipos de operação raiz:
Query(ler),Mutation(escrever e depois ler) eSubscription(um fluxo longo de eventos). Só oQueryé obrigatório. Os campos deles são as portas de entrada da API.
Resolvers
O servidor executa a query campo por campo. Para cada campo ele chama uma função chamada resolver, passando o objeto pai e os argumentos. O schema descreve o formato dos dados; os resolvers dizem de onde os dados vêm.
Erros e null
Uma resposta é um objeto com data e, se algo falhou, errors. Como uma operação pode ter sucesso em parte, a convenção usual (JSON sobre HTTP) responde `200` mesmo com erros, então o cliente precisa sempre ler errors, não só o código HTTP.
Quando um resolver falha, o seu campo vira null. Se o campo é ! (não nulo), null não é permitido, então o null sobe para o pai, e de novo, até o primeiro campo anulável. Se nenhum é encontrado, o data inteiro vira null. Por isso marcar tudo com ! tem um custo: uma pequena falha pode apagar uma grande parte da resposta.
type User { name: String! posts: [Post!]! }
type Post { title: String! }
o title do post 2 falha:
title é String! -> o post 2 não pode ser null
[Post!]! -> a lista não pode ter item null, então não pode ser null
User.posts é ! -> posts não pode ser null
user(id) é anulável -> user vira null
resultado: { "data": { "user": null }, "errors": [ ... ] }Variáveis, aliases e ordem de execução
- Variáveis (
$term) mantêm a estrutura da query separada dos dados. Os valores viajam em um mapa JSON ao lado do documento e são conferidos contra o tipo declarado. Se você colasse o texto dentro da query, um usuário poderia injetar pedaços de query. As variáveis também deixam o mesmo documento ser guardado em cache ou registrado como persisted query. - Aliases (
first: user(id: 1)esecond: user(id: 2)) mudam a chave na resposta. Sem eles, pedir o mesmo campo duas vezes com argumentos diferentes é rejeitado, porque os dois usariam a chaveuser. - Ordem. Os campos de uma query podem rodar em qualquer ordem, até em paralelo, porque não têm efeitos colaterais. Os campos do topo de uma mutation rodam um depois do outro, na ordem em que foram escritos. Isso vale só para o topo; os subcampos rodam normalmente.
O problema N+1
Como cada campo tem o seu resolver, uma lista de N itens com um campo aninhado faz 1 consulta para a lista e depois N consultas para o campo aninhado. Com 20 posts e seus autores, são 1 + 20 = 21 consultas SQL, mesmo que haja só 4 autores diferentes. A solução é um DataLoader: os resolvers pedem uma chave ao loader, o loader reúne todas as chaves do passo atual, remove as repetidas e faz uma consulta só (WHERE id IN (...)). Agora são 1 + 1 = 2. Um loader vive por uma requisição, então nunca vaza linhas para outros clientes.
- Sem batching
- 201 comandos
- Com loader que agrupa
- 3 comandos
Custo, profundidade e cache
Como o cliente escreve a query, o cliente também decide o quanto ela custa. Um tipo que aponta para si mesmo permite aninhamento sem fim, e o custo se multiplica: quatro níveis de friends(first: 100) pedem 100 x 100 x 100 x 100 = 100^4 = 100 milhões de objetos. Contar requisições não basta, porque uma só requisição pode ser abusiva. Os servidores analisam a profundidade (depth) e uma complexidade estimada do documento e o recusam antes de executar.
O cache é mais difícil que no REST. O cache HTTP foi pensado em torno do GET e de uma URL por recurso, mas o GraphQL típico envia tudo com POST para /graphql, e a diferença está no corpo. Saídas: enviar queries por GET, usar persisted queries identificadas por um hash na URL, ou fazer cache no cliente por tipo e id.
- REST (livros inteiros)
- 27.6 kB
- GraphQL (2 campos)
- 3 kB
JSON-RPC e gRPC: chamar uma função distante
Um RPC (Remote Procedure Call, chamada de procedimento remoto) faz chamar outro computador parecer chamar uma função comum. Você não pensa em recursos ou campos: você nomeia uma ação (book.get, sum) e passa parâmetros.
JSON-RPC 2.0
Uma especificação bem pequena. Uma requisição tem jsonrpc: "2.0", method, params opcional e um id. A resposta tem jsonrpc, o mesmo id (para você casar respostas com requisições) e exatamente um entre result e error, nunca os dois. Um error tem um code inteiro, uma message e data opcional.
{ "jsonrpc": "2.0", "method": "book.get", "params": { "id": 7 }, "id": 1 }
{ "jsonrpc": "2.0", "result": { "title": "Example" }, "id": 1 }| Código de erro | Significado |
|---|---|
| -32700 | Parse error: o texto não é JSON válido |
| -32600 | Invalid Request: é JSON, mas não é um objeto de requisição válido |
| -32601 | Method not found: o método não existe |
| -32602 | Invalid params: por exemplo "abc" onde se espera um inteiro |
| -32603 | Internal error: erro interno |
| -32000 a -32099 | Reservados para os erros do próprio servidor. Erros da aplicação usam códigos fora da faixa reservada |
- Notificação (notification): uma requisição sem `id`. O servidor não deve responder nada, nem um erro. Serve para eventos e logs, mas o cliente nunca sabe se falhou.
- Batch (lote): um array de requisições enviadas juntas. O servidor devolve um array com uma resposta para cada requisição que tem
id, em qualquer ordem. Notificações não recebem nada. Um erro em um elemento não afeta os outros. Se o lote só tem notificações, o servidor não devolve nada. - Um lote só economiza round trips para chamadas que não dependem umas das outras.
| Assunto | REST | JSON-RPC |
|---|---|---|
| Remover o livro 42 | DELETE /books/42 | POST /rpc com {"method":"book.delete","params":{"id":42},"id":1} |
| A ação está no | método HTTP | corpo (method) |
| O resultado está no | código de status | result ou error no corpo |
| Transporte | HTTP | qualquer um (HTTP, sockets...) |
gRPC
O gRPC parte de um contrato escrito em Protocol Buffers (um arquivo .proto). O mesmo arquivo define os serviços, as mensagens e a codificação binária compacta, em que cada campo é identificado por um número em vez de um nome. Um compilador gera código tipado em várias linguagens, então chamar o serviço remoto parece chamar uma função local. O gRPC viaja sobre HTTP/2: cada chamada é um stream (cabeçalhos com o nome do método, mensagens em frames DATA e um status final nos trailers), e a multiplexação deixa muitas chamadas dividirem uma conexão.
message Book {
string title = 1;
int32 year = 2;
}
service Library {
rpc Get (BookId) returns (Book); // unary
rpc Upload (stream Chunk) returns (Summary); // client streaming
}| Tipo de chamada | Mensagens | Assinatura |
|---|---|---|
| Unary | 1 requisição, 1 resposta | rpc A (Req) returns (Res) |
| Server streaming | 1 requisição, várias respostas | rpc A (Req) returns (stream Res) |
| Client streaming | várias requisições, 1 resposta | rpc A (stream Req) returns (Res) |
| Bidirecional | várias nos dois sentidos, independentes | rpc A (stream Req) returns (stream Res) |
Deadlines (prazos). O cliente diz até quando está disposto a esperar. Se o tempo acaba, a chamada termina com DEADLINE_EXCEEDED e o servidor é avisado para parar de trabalhar. Em uma cadeia A, B, C o prazo é repassado como tempo restante. Se A dá 500 ms e B usa 200 ms, C recebe 300 ms. Se C precisa de 400 ms, a chamada termina na marca de 500 ms com DEADLINE_EXCEEDED.
t=0 A chama B (prazo = t 500)
t=200 B chama C (restam 300 ms)
t=500 o prazo estoura -> DEADLINE_EXCEEDED, C é cancelado
(C terminaria em t 600)- REST
- 3 round trips
- JSON-RPC
- 2 round trips
- GraphQL
- 1 round trips
WebSocket e server-sent events: o servidor fala primeiro
No HTTP puro o cliente sempre pergunta primeiro. Mas um chat, um jogo ou um painel de cotações precisa que o servidor envie novidades por conta própria. Há quatro maneiras de fazer isso.
| Técnica | Como funciona | Bom para |
|---|---|---|
| Polling | O cliente pergunta de novo a cada poucos segundos | Mudanças raras |
| Long polling | O servidor segura a requisição aberta até ter novidade; uma requisição por mensagem | Notificações simples |
| Server-sent events (SSE) | Uma resposta HTTP que nunca termina; mão única, do servidor para o cliente | Painéis, feeds, barras de progresso |
| WebSocket | Uma conexão, duas vias, texto ou binário | Chat, jogos, colaboração |
WebSocket
Um WebSocket começa como um GET HTTP/1.1 comum que pede um upgrade. Se o servidor concorda, responde 101 Switching Protocols, e daí em diante a mesma conexão TCP fala o protocolo WebSocket. O ws:// roda sobre TCP puro e o wss:// roda dentro do TLS (como http:// e https://).
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: AAECAwQFBgcICQoLDA0ODw==
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Accept: Bz3qJYTGdOe8gUSpLosEdiLKDrk=- O
Sec-WebSocket-Accepté um SHA-1uma função que transforma qualquer texto em uma impressão digital curta da chave do cliente mais um texto público fixo, escrito em Base64um jeito de escrever bytes usando só letras, dígitos e alguns símbolos. Ele só prova que o servidor entendeu WebSocket e respondeu a esta requisição. Não autentica ninguém e não criptografa nada. Isso é trabalho do TLS e do seu aplicativo. - Depois do handshake, os dados andam em frames de texto (UTF-8) ou binários. Existem três frames de controle: ping e pong (um batimento cardíaco: 'você está aí?' / 'sim') e close (iniciar uma despedida ordenada com um código e um motivo).
- Masking (máscara): todo frame vindo do cliente passa por um XOR com uma chave aleatória de 32 bits enviada no próprio frame. Isso não esconde os dados. Protege proxies defeituosos de serem enganados a tratar os bytes como HTTP (cache poisoning), porque não se confia que o script da página escolha os bytes exatos. Os frames do servidor não são mascarados.
Server-sent events
O SSE é só uma resposta HTTP normal que não termina. O servidor usa Content-Type: text/event-stream e continua escrevendo texto. O navegador lê isso pela API EventSource. Para responder, o cliente faz outras requisições HTTP comuns.
: keep-alive <- um comentário (começa com dois-pontos)
retry: 5000 <- reconecte depois de 5 segundos
id: 42
event: price
data: 11
- Campos:
data(o conteúdo),event(o nome; sem ele o nome émessage),id(a posição no fluxo) eretry. - Um evento é entregue só quando chega a linha vazia que fecha o seu bloco. Várias linhas
datano mesmo bloco viram um valor só, unido com quebras de linha. Um bloco que ainda não fechou não foi entregue. - Sem
event:o handleronmessageé chamado. Comevent: updatesó são chamados os listeners adicionados comaddEventListener('update', ...). - Se a conexão cai, o
EventSourceespera o tempo doretrye pede de novo, enviando `Last-Event-ID` com o últimoidque viu. O servidor consegue retomar sem perder eventos.
Juntando tudo
Agora dá para ver como as peças se empilham. Quando você abre https://exemplo.test pela primeira vez, o navegador inicia uma conexão TCP, faz um handshake TLS 1.3 (o SNI escolhe o certificado, o ALPN escolhe h2) e então envia requisições HTTP/2. As respostas trazem Cache-Control para que da próxima vez seja mais rápido. O servidor pode acrescentar alt-svc para que mais tarde você use HTTP/3.
| Necessidade | Escolha | Por quê |
|---|---|---|
| API pública e duradoura, com recursos e cache HTTP | REST | Ferramentas padrão e cache de GET |
| Muitas telas, cada uma precisando de uma fatia diferente de dados conectados | GraphQL | Uma requisição, só os campos desejados. Exige batching e limites de custo |
| Ações que não são recursos, ou serviços internos | JSON-RPC | O mais simples de construir, independe do transporte |
| Chamadas rápidas entre serviços, com contrato rígido | gRPC | Binário, tipado, streaming e deadlines do HTTP/2 |
| O servidor só empurra atualizações | SSE | HTTP comum, reconexão automática |
| Os dois lados enviam com frequência | WebSocket | Duas vias, pouco custo por mensagem |
| Camada | Pergunta que responde | Ideias-chave |
|---|---|---|
| Semântica do HTTP | O que a mensagem significa? | Métodos (seguro, idempotente), códigos de status, cabeçalhos, delimitação |
| Cache, cookies, CORS | O que pode ser reaproveitado, lembrado ou lido? | max-age, no-store, revalidação, Vary, SameSite, origem |
| TLS | É privado e é o servidor certo? | Certificados, acordo de chaves, 1 contra 2 round trips, forward secrecy |
| HTTP/1.1, 2, 3 | Como as mensagens vão para o fio? | Multiplexação, HPACK/QPACK, QUIC, head-of-line blocking |
| Estilos de API | O que o cliente nomeia e quem molda a resposta? | Recurso, campos, procedimento |
Erros comuns
- Achar que
no-cacheproíbe guardar (ele quer dizer 'confira primeiro'). - Misturar 401 (quem é você?) com 403 (você não pode).
- Acreditar que o CORS protege o servidor (ele protege a leitura da resposta, nos navegadores).
- Achar que erros do GraphQL usam códigos 4xx ou 5xx (a convenção é
200comerrors). - Achar que o HTTP/3 é sempre mais rápido (ele ajuda mais com latência alta ou perdas).
- Esquecer que o WebSocket precisa de uma checagem de
Originno servidor. - Reutilizar um número de campo do Protocol Buffers.