Dia 5 — HTTP: o que o servidor faz

Informatica · Conteudo · publicado em 30/09/2026
Dia 5 de 16

HTTP: o que o servidor faz

Aula 1

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étodoPromete
GETleia, não muda nada
POSTcrie algo novo
PUTsubstitua o recurso inteiro
PATCHmude uma parte
DELETEapague

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ódigoSignifica
200deu certo, o corpo tem o resultado
201criou recurso novo
204deu certo, e não há corpo para devolver
400o pedido chegou errado (validação)
404o caminho ou o registro não existe
409o pedido faz sentido, mas conflita (duplicado)
500o 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().
Aula 2

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 reqO que é
req.methodGET, POST, PUT, DELETE
req.urlo caminho pedido, com a query string
req.headerscabeçalhos que o cliente enviou
Em resO que é
res.writeHead(status, cabecalhos)monta linha de status e cabeçalhos
res.statusCode = 404o 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()`.