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.
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:
/api/echo?color=red&size=10Aqui 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.
{
"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:
https://api.testingapis.com/api/Products?category=books&limit=5| Parte | Valor | Significado |
|---|---|---|
| Esquema | https | Como se conectar. https é HTTP dentro de uma conexão criptografada. |
| Host | api.testingapis.com | Qual servidor. Também é enviado como o cabeçalho Host. |
| Caminho | /api/Products | Qual recurso nesse servidor. |
| Query string | category=books&limit=5 | Parâ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:
/api/echo?q=red%20shoes&tag=a%26b{
"queryString": "?q=red%20shoes&tag=a%26b",
"query": { "q": "red shoes", "tag": "a&b" }
}Agora envie a tag sem codificar o "e comercial":
/api/echo?tag=a&b{
"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çalho | O que diz | Exemplo |
|---|---|---|
Content-Type | O formato do corpo que você está enviando. | application/json |
Accept | Os formatos que você aceita receber. | application/json |
User-Agent | Qual cliente enviou a requisição. | curl/8.18.0 |
Authorization | Credenciais, como um token. | Bearer abc123 |
Host | Qual 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:
/api/formats/negotiateCompare 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:
curl -X POST https://api.testingapis.com/api/echo \
-H "Content-Type: application/json" \
-d '{"title":"Dune","year":1965}'{
"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:
/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:
curl -X POST https://api.testingapis.com/api/echo \
-d '{"title":"Dune"}'{
"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:
"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 Paulopela 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-agentpelo navegador e pelo curl. Por que uma API se importaria com qual dos dois você usou?