Dia 5 — HTTP: o que o servidor faz
O protocolo HTTP, sem mistério
O protocolo HTTP, sem mistério
HTTP é o protocolo de conversa entre cliente e servidor. O cliente faz uma
requisição; o servidor devolve uma resposta. Não há mais nada — tudo o que
existe em volta é detalhe dessa troca.
Uma requisição tem: método (a intenção), URL (o caminho), headers
(metadados) e, em alguns métodos, corpo da requisição (os dados). Uma
resposta tem: status (o resultado), headers e corpo.
POST /alunos HTTP/1.1 Host: 127.0.0.1:3000 Content-Type: application/json {"nome": "Ana", "turma": 3}
HTTP/1.1 201 Created Content-Type: application/json Content-Length: 27 {"id": 7, "nome": "Ana"}
O cabeçalho e o corpo são separados por uma linha em branco. É essa linha que
diz onde acaba o cabeçalho — e é por isso que o exemplo mede a resposta crua: é o
que o painel de rede de qualquer navegador mostra.
Os métodos, e o que cada um promete
| Método | Promete |
|---|---|
GET | leia, não muda nada |
POST | crie algo novo |
PUT | substitua o recurso inteiro |
PATCH | mude uma parte |
DELETE | apague |
O método é o que o cliente pede; o status é o que o servidor responde. E
o servidor tem direito de recusar: um POST em rota que só aceita GET volta
405 Method Not Allowed, não 200.
Os status que importam
O status code é o número de três dígitos que abre a resposta, e ele vem
sempre com um texto: 200 OK, 404 Not Found. O número é o que o código
compara; o texto é o que a pessoa lê.
| Código | Significa |
|---|---|
200 | deu certo, o corpo tem o resultado |
201 | criou recurso novo |
204 | deu certo, e não há corpo para devolver |
400 | o pedido chegou errado (validação) |
404 | o caminho ou o registro não existe |
409 | o pedido faz sentido, mas conflita (duplicado) |
500 | o servidor quebrou ao tentar atender |
O código é o que decide o que o cliente faz. Um servidor que devolve 200 num
erro obriga quem chama a ler o corpo para descobrir que deu errado — e aí o
cliente erra por padrão, porque confia no 200 e segue em frente.
A resposta crua tem ainda cabeçalhos que variam a cada execução e que
portanto não interessam aqui: date (a hora do servidor), connection e
keep-alive (negociação do cliente). A porta do servidor também muda, porque o
exemplo pede uma porta livre ao sistema com listen(0) — é o mesmo mecanismo da
aula 2, e a razão de ele não usar 3000 fixo.
HTTPS é o mesmo com TLS na frente
Nada muda de protocolo: é HTTP dentro de um canal criptografado, negociado antes
do primeiro byte. A diferença é que a URL troca http:// por https:// e a
porta padrão é a 443 em vez da 80.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 5: HTTP sem mistério, medido no fio. // // Nenhum framework, nenhuma rota: um `http.createServer` que responde a // qualquer requisicao, e o cliente `fetch` do proprio Node falando com ele. // // O que a aula mostra e o ANATOMICO de uma resposta HTTP: linha de status com // codigo e frase, cabecalhos, e corpo separado por uma linha em branco. O que o // exemplo mede de verdade e esse cabecalho cru, porque e ele que a pagina de // rede de qualquer navegador mostra e ninguem costuma ler. const http = require('node:http'); async function main() { // `listen(0)` pede uma porta LIVRE ao sistema. Porta fixa (3000) falha em // qualquer maquina que ja tenha um processo escutando nela, e o exemplo // passaria a falhar por motivo aleatorio. A porta e diferente a cada execucao // e isso e esperado: e a unica linha da saida que muda. const servidor = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('ok'); }); await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve)); const { port } = servidor.address(); try { console.log('servidor no ar em http://127.0.0.1:' + port); console.log('(a porta muda a cada execucao: o 0 pediu uma livre ao sistema)'); // --- 1. o GET mais simples que existe --- const resposta = await fetch(`http://127.0.0.1:${port}/`); console.log(''); console.log('--- o que o cliente recebe ---'); console.log('status:', resposta.status, resposta.statusText); console.log('content-type:', resposta.headers.get('content-type')); console.log('corpo:', await resposta.text()); // --- 2. os metodos HTTP, um por vez --- // // O metodo vai na requisicao e diz a intencao. O status da resposta e o que // o servidor devolve: 200 deu certo, 201 criou, 404 nao encontrou, 400 foi // pedido invalido, 500 o servidor quebrou. O metodo que o cliente pede e a // decisao do servidor: um POST numa rota que so aceita GET tem de voltar // 405, e nao 200. console.log(''); console.log('--- metodos e status ---'); for (const metodo of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) { const r = await fetch(`http://127.0.0.1:${port}/qualquer`, { method: metodo }); console.log(' ' + metodo.padEnd(7) + '-> status', r.status); } // --- 3. cabecalho e corpo sao separados por uma linha em branco --- // // Esta e a resposta HTTP crua, e e o que o painel de rede do navegador // mostra. O status vem na primeira linha; os cabecalhos vem ate a linha em // branco; o que sobrar e o corpo. // // Dois cabecalhos variesem a cada execucao e o exemplo NAO os imprime: // `date` (a hora do servidor) e `connection`/`keep-alive` (negociacao do // cliente). A pagina embute a saida real, entao um `date` trocando a cada // rodada faria a pagina parecer errada. O que interessa aqui sao a primeira // linha e os cabecalhos de conteudo — e sao justos os estaveis. const bruto = await new Promise((resolve, reject) => { const req = require('node:http').request( { host: '127.0.0.1', port, path: '/', method: 'GET' }, (r) => { const linhas = [`HTTP/${r.httpVersion} ${r.statusCode} ${r.statusMessage}`]; for (const [nome, valor] of Object.entries(r.headers)) { if (nome === 'date' || nome === 'connection' || nome === 'keep-alive') continue; linhas.push(`${nome}: ${valor}`); } r.resume(); r.on('end', () => resolve(linhas.join('\n') + '\n\n(corpo: "ok" — vem depois desta linha em branco)')); } ); req.on('error', reject); req.end(); }); console.log(''); console.log('--- a resposta HTTP crua ---'); console.log(bruto); console.log(''); console.log('cabecalhos omitidos: date, connection, keep-alive (variam a cada execucao)'); // --- 4. os status que importam de verdade --- // // O codigo e o que o cliente automatizado (fetch, curl, um app de celular) // le para decidir o que fazer. Um servidor que devolve 200 num erro obriga // quem chama a ler o corpo para descobrir que deu errado. console.log('--- os codigos que o material usa ---'); const tabela = [ [200, 'deu certo, o corpo tem o resultado'], [201, 'criu recurso novo'], [204, 'deu certo, e nao ha corpo para devolver'], [400, 'o pedido chegou errado (validacao)'], [404, 'o caminho ou o registro nao existe'], [409, 'o pedido faz sentido mas conflita (duplicado)'], [500, 'o servidor quebrou ao tentar atender'], ]; for (const [codigo, uso] of tabela) { console.log(' ' + codigo + ' - ' + uso); } console.log(''); console.log('o exemplo acima devolveu 200 para os cinco metodos, porque a rota'); console.log('ainda nao existe: e o servidor da aula 2 que decide status por rota.'); } finally { // `server.close()` no `finally`, sempre. Sem ele o socket segura o event loop // e o processo fica parado esperando conexao que nunca chega. await new Promise((resolve) => servidor.close(resolve)); console.log(''); console.log('servidor encerrado com close().'); } } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
servidor no ar em http://127.0.0.1:39109 (a porta muda a cada execucao: o 0 pediu uma livre ao sistema) --- o que o cliente recebe --- status: 200 OK content-type: text/plain; charset=utf-8 corpo: ok --- metodos e status --- GET -> status 200 POST -> status 200 PUT -> status 200 DELETE -> status 200 PATCH -> status 200 --- a resposta HTTP crua --- HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 transfer-encoding: chunked (corpo: "ok" — vem depois desta linha em branco) cabecalhos omitidos: date, connection, keep-alive (variam a cada execucao) --- os codigos que o material usa --- 200 - deu certo, o corpo tem o resultado 201 - criu recurso novo 204 - deu certo, e nao ha corpo para devolver 400 - o pedido chegou errado (validacao) 404 - o caminho ou o registro nao existe 409 - o pedido faz sentido mas conflita (duplicado) 500 - o servidor quebrou ao tentar atender o exemplo acima devolveu 200 para os cinco metodos, porque a rota ainda nao existe: e o servidor da aula 2 que decide status por rota. servidor encerrado com close().
Criar o primeiro servidor HTTP
Criar o primeiro servidor HTTP
O servidor de Node é o http module nativo, node:http, e ele não precisa de
framework para atender requisição. http.createServer recebe uma função e
devolve um servidor.
const http = require('node:http'); const servidor = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('ok'); }); servidor.listen(3000, '127.0.0.1', () => console.log('no ar'));
req e res: os dois lados
A função da requisição é chamada uma vez por pedido, e recebe os dois lados da
conversa: req é o que chegou e res é o objeto que monta o response que vai
sair.
Em req | O que é |
|---|---|
req.method | GET, POST, PUT, DELETE |
req.url | o caminho pedido, com a query string |
req.headers | cabeçalhos que o cliente enviou |
Em res | O que é |
|---|---|
res.writeHead(status, cabecalhos) | monta linha de status e cabeçalhos |
res.statusCode = 404 | o mesmo, em duas etapas |
res.write(pedaco) | escreve um pedaço e mantém a resposta aberta |
res.end() | fecha a resposta |
res.end(corpo) | fecha a resposta com o corpo |
res.end() é o que encerra a resposta. Sem ele o cliente fica esperando e a
requisição trava — é o defeito mais comum de quem está começando servidor.
listen: porta, endereço e a espera
listen(porta, endereco, callback) liga o socket. O callback é o equivalente a
esperar o evento 'listening', e sem ele o fetch seguinte corre contra um
socket que ainda não subiu. Por isso o padrão do material é sempre o mesmo:
await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve)); const { port } = servidor.address();
listen(0) pede ao sistema uma porta livre. A porta 3000 fixa falha em
qualquer máquina que já tenha um processo escutando nela, e o exemplo passa a
falhar por motivo aleatório. A consequência é que **a porta muda a cada
execução** — a saída embutida mostra http://127.0.0.1:40767 numa rodada e
:37243 na outra, e isso é o comportamento certo, não um defeito.
O nome localhost é o que se escreve no navegador para chegar nesse processo:
ele resolve para 127.0.0.1, que é a própria máquina.
É assim que se sobe o servidor e se testa API ao mesmo tempo — o exemplo imprime
a URL com a porta que o sistema entregou, e o fetch seguinte usa exatamente
essa URL.
O close() no finally
Um programa que sobe servidor e não fecha nunca termina: o socket seguro o
event loop e o processo fica parado. Por isso todo exemplo fecha no finally,
que roda mesmo se a requisição falhar.
try { const resposta = await fetch(`http://127.0.0.1:${port}/`); } finally { await new Promise((resolve) => servidor.close(resolve)); }
Testar com curl
Testar API na mão é o que o curl faz: curl -i http://127.0.0.1:3000/ mostra
a resposta crua — linha de status, cabeçalhos, linha em branco e corpo. `-X
POST troca o método, -H acrescenta cabeçalho, -d manda corpo. O fetch` do
Node faz o mesmo e ainda fica disponível dentro do próprio exemplo, o que permite
testar sem sair do processo.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 5: o primeiro servidor HTTP de verdade. // // Um `http.createServer` que sobe, atende e DERROBA. O processo inteiro dura // menos de um segundo, e esse e o ponto: um programa de Node que sobe servidor // e nao fecha nunca termina, e e por isso que o `close()` vai no `finally`. // // As quatro coisas que este exemplo tem de acertar, e o portao exige: // 1. `listen(0)` — porta livre pedida ao sistema, nunca 3000 fixa // 2. esperar o 'listening' antes de chamar, com `await new Promise` // 3. `server.close()` no `finally`, sempre, mesmo se a requisicao falhar // 4. uma linha de `console.log` de contexto ANTES de cada erro const http = require('node:http'); async function main() { // `createServer` recebe uma funcao `(req, res)`. Ela roda uma vez por // requisicao, e as duas coisas que importam sao: // `req` — o que o cliente pediu (metodo, url, headers) // `res` — o que o servidor vai devolver const servidor = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('servidor no ar'); }); // `listen(0, '127.0.0.1', callback)`: o 0 e o pedido de porta livre. A // callback e o equivalente a esperar o evento 'listening' — sem ela, o // `fetch` seguinte corre contra um socket que ainda nao subiu. await new Promise((resolve) => servidor.listen(0, '127.0.0.1', resolve)); const { port } = servidor.address(); try { console.log('servidor no ar em http://127.0.0.1:' + port); console.log('(a porta e diferente a cada execucao: o 0 pediu uma livre ao sistema)'); // `res.end(corpo)` e o que FECHA a resposta. Sem ele o cliente fica esperando // e a requisicao trava. `res.write` escreve um pedaco e mantem a resposta // aberta; `res.end` sem argumento fecha sem corpo. const resposta = await fetch(`http://127.0.0.1:${port}/`); console.log(''); console.log('--- GET / ---'); console.log('status:', resposta.status); console.log('corpo:', await resposta.text()); // `req.url` e o caminho pedido. Nesta versao ele responde 200 para QUALQUER // caminho: e a aula de rotas, do dia 6, que faz o servidor diferenciar. const outra = await fetch(`http://127.0.0.1:${port}/qualquer/coisa`); console.log(''); console.log('--- GET /qualquer/coisa ---'); console.log('o req.url chegou como:', outra.url.split(String(port))[1]); console.log('status:', outra.status, '| o mesmo servidor responde para qualquer caminho agora'); // O mesmo servidor visto pelo `http.request` cru, que e o que o `curl` // faz por baixo e o que o aluno vai usar para depurar. // // O `Connection: close` nao e detalhe: sem ele o socket fica de pe depois // da resposta, o `fetch` reaproveita a conexao e um programa de linha de // comando esperando a child process terminar fica esperando para sempre. const bruto = await new Promise((resolve, reject) => { const req = http.request( { host: '127.0.0.1', port, path: '/', method: 'GET', headers: { Connection: 'close' } }, (r) => { let corpo = ''; r.on('data', (pedaco) => { corpo += pedaco; }); r.on('end', () => { const linhas = [`HTTP/${r.httpVersion} ${r.statusCode} ${r.statusMessage}`]; for (const [nome, valor] of Object.entries(r.headers)) { if (nome === 'date') continue; // a hora do servidor: muda sempre linhas.push(`${nome}: ${valor}`); } resolve(linhas.join('\n') + '\n\n' + corpo); }); } ); req.on('error', reject); req.end(); }); console.log(''); console.log('--- a mesma resposta, em HTTP cru (e o que o curl -i mostra) ---'); console.log(bruto.trim()); console.log(''); console.log('(o cabecalho `date` foi omitido: e a hora do servidor e muda a cada execucao)'); console.log('para testar na mao: curl -i http://127.0.0.1:' + port + '/'); // `res.writeHead` monta a linha de status e os cabecalhos de uma vez; // `res.statusCode = ...` faz o mesmo em duas etapas. Com o cabecalho de // conteudo faltando, o navegador tenta adivinhar o tipo e a API volta // download em vez de JSON. console.log(''); console.log('--- cabecalho de conteudo muda o que o cliente faz ---'); console.log('sem content-type, o navegador adivinha (e erra)'); console.log('com content-type: application/json, o fetch usa resposta.json()'); } catch (erro) { // `console.error` vai para o stderr, que NAO chega na pagina. O par abaixo // e o que o CONTRATO.md exige: o terminal mostra o erro, a pagina tambem. console.error('falha na requisicao: ' + erro.code + ' - ' + erro.message); console.log(' erro na requisicao:', erro.code, '-', erro.message); process.exitCode = 1; } finally { // O `finally` e o que impede o processo de travar: sem ele o servidor fica // segurando o event loop e o processo fica parado esperando conexao. await new Promise((resolve) => servidor.close(resolve)); console.log(''); console.log('servidor encerrado com close().'); } // A prova de que o processo nao ficou pendurado: se o `close()` tivesse // falhado, o `server.close()` da linha de baixo nunca chegaria aqui. console.log('o processo terminou sozinho, sem `process.exit()`.'); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
servidor no ar em http://127.0.0.1:42699 (a porta e diferente a cada execucao: o 0 pediu uma livre ao sistema) --- GET / --- status: 200 corpo: servidor no ar --- GET /qualquer/coisa --- o req.url chegou como: /qualquer/coisa status: 200 | o mesmo servidor responde para qualquer caminho agora --- a mesma resposta, em HTTP cru (e o que o curl -i mostra) --- HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 connection: close transfer-encoding: chunked servidor no ar (o cabecalho `date` foi omitido: e a hora do servidor e muda a cada execucao) para testar na mao: curl -i http://127.0.0.1:42699/ --- cabecalho de conteudo muda o que o cliente faz --- sem content-type, o navegador adivinha (e erra) com content-type: application/json, o fetch usa resposta.json() servidor encerrado com close(). o processo terminou sozinho, sem `process.exit()`.