Anatomia de uma requisição

Método, URL, cabeçalhos e corpo, e o que o servidor realmente recebe.

Na lição anterior você enviou uma requisição digitando uma URL no navegador. O navegador preencheu todo o resto por você. Esta lição desmonta uma requisição para você saber o que é cada peça, porque todo teste que você escrever vai definir algumas dessas peças de propósito e conferir como o servidor reage.

Uma requisição HTTP tem quatro partes:

  • um método, que diz que tipo de ação você quer
  • uma URL, que diz em qual coisa, em qual servidor, você quer que ela seja feita
  • cabeçalhos, que levam informações extras sobre a requisição
  • um corpo opcional, que leva os dados que você está enviando

Como é uma requisição no fio

HTTP/1.1 é um protocolo de texto. Antes de qualquer criptografia, uma requisição são algumas linhas de texto puro. Esta é a requisição que o curl envia para https://api.testingapis.com/api/echo?color=red. Você mesmo pode vê-la rodando o curl com -v (modo detalhado): as linhas que começam com > são o que saiu.

Requisição crua
GET /api/echo?color=red HTTP/1.1
Host: api.testingapis.com
User-Agent: curl/8.18.0
Accept: */*

A primeira linha é a linha de requisição: o método, o caminho com a query string e a versão do protocolo. Cada linha depois dela é um cabeçalho, um nome e um valor separados por dois-pontos. Uma linha vazia encerra os cabeçalhos. Se houvesse corpo, ele viria depois dessa linha vazia. As versões mais novas do HTTP (HTTP/2 e HTTP/3) enviam a mesma informação em formato binário, mas as partes são idênticas, e toda ferramenta continua mostrando tudo nessa forma de texto.

Vendo o que o servidor recebeu

O jeito mais fácil de aprender sobre requisições é mandar uma para um servidor que diz o que recebeu. /api/echo faz exatamente isso. Ele aceita qualquer método e responde com uma descrição da sua requisição. Envie esta:

GET/api/echo?color=red&size=10

Aqui está a resposta para o curl, cortada nos campos interessantes. Se você enviar pela página, os cabeçalhos serão os do seu navegador.

JSON
{
  "method": "GET",
  "url": "http://api.testingapis.com/api/echo?color=red&size=10",
  "path": "/api/echo",
  "queryString": "?color=red&size=10",
  "query": { "color": "red", "size": "10" },
  "headers": {
    "Accept": "*/*",
    "Host": "api.testingapis.com",
    "User-Agent": "curl/8.18.0",
    "Accept-Encoding": "gzip, br",
    "Cf-Ipcountry": "IN",
    "X-Forwarded-For": "203.0.113.7, 172.71.198.178, 10.30.107.46",
    "X-Forwarded-Proto": "https"
  },
  "contentType": null,
  "contentLength": 0,
  "body": null
}

Duas coisas ali merecem uma segunda olhada. O servidor viu a URL como http://, não https://. E recebeu cabeçalhos que você nunca enviou, como Cf-Ipcountry e X-Forwarded-For. A sua requisição passou por uma CDN e um balanceador de carga antes de chegar à aplicação, e cada um reescreveu ou acrescentou algo. O primeiro endereço em X-Forwarded-For é o seu (o de cima é só um exemplo). Isso é normal em sistemas de produção, e é por isso que um endpoint de eco é útil: ele mostra o que chegou, não o que você acha que enviou.

O método

O método é o verbo. GET pede dados. POST envia dados para criar algo. PUT, PATCH e DELETE alteram ou removem coisas. A barra de endereços do navegador só envia GET, e por isso você precisa de outras ferramentas para o resto. Os métodos têm uma lição só deles, Métodos HTTP, então por enquanto basta saber que a mesma URL pode fazer coisas diferentes dependendo do método.

A URL

Quebre esta URL em partes:

URL
https://api.testingapis.com/api/Products?category=books&limit=5
ParteValorSignificado
EsquemahttpsComo se conectar. https é HTTP dentro de uma conexão criptografada.
Hostapi.testingapis.comQual servidor. Também é enviado como o cabeçalho Host.
Caminho/api/ProductsQual recurso nesse servidor.
Query stringcategory=books&limit=5Parâmetros extras, depois do ?, separados por &.

URLs também podem ter uma porta (:8080 depois do host) e um fragmento (#section no fim). Você vai ver portas com frequência em ambientes de teste e locais. Fragmentos nunca chegam ao servidor: o navegador fica com eles.

Parâmetros de caminho e parâmetros de consulta

Valores podem viajar no caminho ou na query string. Em /api/Products/1, o 1 é um parâmetro de caminho. Ele identifica um produto específico, e a documentação escreve o caminho como /api/Products/{id}. Em /api/Products?limit=5, limit é um parâmetro de consulta. Parâmetros de consulta geralmente filtram, ordenam ou paginam resultados, e geralmente são opcionais. Essa divisão é uma convenção, não uma regra, então confira a documentação de cada API.

Caminhos costumam diferenciar maiúsculas de minúsculas, e os nomes dos parâmetros de consulta geralmente também. Isso depende do framework do servidor, então faça um teste rápido em qualquer API nova: envie /api/products em vez de /api/Products e veja o que acontece.

Codificação percentual

Alguns caracteres têm uma função dentro de uma URL. ? começa a query string, & separa parâmetros, # começa o fragmento, / separa segmentos do caminho, e espaços não são permitidos de jeito nenhum. Para enviar um deles como dado, você o troca por % e o valor hexadecimal do byte. Um espaço vira %20 e & vira %26. Isso se chama codificação percentual, ou codificação de URL.

Digamos que você queira buscar "red shoes" e filtrar por uma tag cujo valor é literalmente o texto a&b. Codificado, funciona:

GET/api/echo?q=red%20shoes&tag=a%26b
Resposta cortada
{
  "queryString": "?q=red%20shoes&tag=a%26b",
  "query": { "q": "red shoes", "tag": "a&b" }
}

Agora envie a tag sem codificar o "e comercial":

GET/api/echo?tag=a&b
Resposta cortada
{
  "queryString": "?tag=a&b",
  "query": { "tag": "a", "b": "" }
}

Agora o servidor vê dois parâmetros: tag com o valor a, e um parâmetro vazio chamado b. Nenhum erro, nenhum aviso. A requisição "funcionou" e os dados estão errados. Essa é uma classe real de bug em clientes que montam URLs colando strings, e é por isso que campos de busca são um bom lugar para digitar &, sinais de mais, barras e texto fora do inglês durante os testes. O codificador de URL deste site faz a conversão se você precisar fazer à mão.

Cabeçalhos

Cabeçalhos são pares de nome e valor que descrevem a requisição. Os nomes não diferenciam maiúsculas de minúsculas, então content-type e Content-Type significam a mesma coisa. A maioria dos clientes adiciona vários cabeçalhos automaticamente. Os que você mais vai definir ou conferir são estes:

CabeçalhoO que dizExemplo
Content-TypeO formato do corpo que você está enviando.application/json
AcceptOs formatos que você aceita receber.application/json
User-AgentQual cliente enviou a requisição.curl/8.18.0
AuthorizationCredenciais, como um token.Bearer abc123
HostQual site no servidor. Os clientes o preenchem a partir da URL.api.testingapis.com

Accept importa quando uma API consegue responder em mais de um formato. /api/formats/negotiateentre para ver devolve o mesmo usuário em JSON por padrão e em XML se você pedir:

GET/api/formats/negotiate
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita

Compare o XML com o JSON que você recebe sem o cabeçalho. Os dois trazem os mesmos sete campos, escritos do jeito de cada formato: a lista roles vira elementos role repetidos, e o manager nulo vira um elemento vazio marcado com xsi:nil. Quando uma API oferece vários formatos, teste cada um e não presuma que batem. Quando esta lição foi escrita, faltavam três campos na versão XML deste usuário.

Authorization é tratado direito em Autenticação e autorização. Uma coisa para saber desde já: nunca cole tokens reais em capturas de tela, relatórios de bug ou coleções compartilhadas. Eles são senhas.

O corpo

Requisições GET normalmente não têm corpo. POST, PUT e PATCH geralmente têm, porque enviam dados ao servidor. O corpo pode estar em qualquer formato, e o cabeçalho Content-Type diz ao servidor como lê-lo. Na maioria das APIs de hoje, o corpo é JSON. Isto envia um pequeno objeto JSON para o endpoint de eco com o curl:

Terminal
curl -X POST https://api.testingapis.com/api/echo \
  -H "Content-Type: application/json" \
  -d '{"title":"Dune","year":1965}'
Resposta cortada
{
  "method": "POST",
  "path": "/api/echo",
  "query": {},
  "contentType": "application/json",
  "contentLength": 28,
  "body": "{\"title\":\"Dune\",\"year\":1965}",
  "json": { "title": "Dune", "year": 1965 }
}

O servidor leu o cabeçalho, viu application/json e interpretou os 28 bytes como um objeto. Você pode enviar a mesma requisição daqui:

POST/api/echo
{"title":"Dune","year":1965}

Quando o Content-Type e o corpo discordam

Agora envie o mesmo tipo de corpo com o curl, mas deixe o cabeçalho de fora:

Terminal
curl -X POST https://api.testingapis.com/api/echo \
  -d '{"title":"Dune"}'
Resposta cortada
{
  "contentType": "application/x-www-form-urlencoded",
  "contentLength": 16,
  "body": "{\"title\":\"Dune\"}",
  "json": null,
  "form": { "{\"title\":\"Dune\"}": "" }
}

A opção -d do curl usa por padrão application/x-www-form-urlencoded, o formato dos formulários HTML. O servidor acreditou no cabeçalho, tentou ler o corpo como formulário e acabou com um único campo cujo nome é o texto JSON inteiro. Nenhum erro voltou. Muitos servidores reais se comportam assim, e outros devolvem 415 Unsupported Media Type. Um Content-Typeausente ou errado é um dos motivos mais comuns de uma requisição "parecer certa" e mesmo assim falhar, então é a primeira coisa a conferir quando um servidor ignora os seus dados.

Se o cabeçalho estiver certo mas o JSON estiver quebrado, um bom servidor avisa. Aqui está parte da resposta do eco para o corpo {"title":"Dune",}, que tem uma vírgula sobrando no fim:

Resposta cortada
"json": null,
"jsonError": "The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. LineNumber: 0 | BytePositionInLine: 16."

Exercício

Use /api/echo para responder cada pergunta. Olhe a resposta toda vez em vez de chutar.

  • Envie /api/echo?name=Zoë&city=São Paulo pela barra de endereços do navegador. O que o navegador mudou na URL antes de enviar, e para o que o servidor decodificou?
  • Envie /api/echo?id=1&id=2. Como o servidor representa um parâmetro que aparece duas vezes? Frameworks diferentes discordam aqui, o que torna isso uma boa pergunta para qualquer API que você testar.
  • Envie um POST com corpo JSON e Content-Type: text/plain. O servidor interpretou o JSON mesmo assim?
  • Experimente /api/echo/user-agent pelo navegador e pelo curl. Por que uma API se importaria com qual dos dois você usou?

Pratique e vá além