Anatomia de uma Transação HTTP
O objetivo deste guia é fornecer uma compreensão sólida do processo de tratamento HTTP do Node.js. Assumiremos que você sabe, de forma geral, como as solicitações HTTP funcionam, independentemente da linguagem ou ambiente de programação. Também assumiremos um pouco de familiaridade com Node.js EventEmitters e Streams. Se você não estiver familiarizado com eles, vale a pena dar uma rápida leitura nos documentos da API de cada um.
Criar o Servidor
Qualquer aplicação de servidor web node terá em algum momento que criar um objeto de servidor web. Isso é feito usando createServer
.
const http = require('node:http')
const server = http.createServer((request, response) => {
// a mágica acontece aqui!
})
A função passada para createServer
é chamada uma vez para cada solicitação HTTP feita contra aquele servidor, então é chamada de manipulador de solicitação. Na verdade, o objeto Server retornado por createServer
é um EventEmitter, e o que temos aqui é apenas um atalho para criar um objeto de servidor e depois adicionar o listener mais tarde.
const server = http.createServer()
server.on('request', (request, response) => {
// o mesmo tipo de mágica acontece aqui!
})
Quando uma solicitação HTTP atinge o servidor, o node chama a função de manipulador de solicitação com alguns objetos úteis para lidar com a transação, solicitação e resposta. Veremos isso em breve. Para realmente servir solicitações, o método listen
precisa ser chamado no objeto do servidor. Na maioria dos casos, tudo o que você precisará passar para listen
é o número da porta em que deseja que o servidor escute. Há também outras opções, então consulte a referência da API.
Método, URL e Cabeçalhos
Ao lidar com uma solicitação, a primeira coisa que você provavelmente quererá fazer é verificar o método e a URL, para que ações apropriadas possam ser tomadas. O Node.js torna isso relativamente fácil, colocando propriedades úteis no objeto de solicitação.
const { method, url } = request
O objeto de solicitação é uma instância de IncomingMessage
. O método aqui sempre será um verbo/método HTTP normal. A url é a URL completa sem o servidor, protocolo ou porta. Para uma URL típica, isso significa tudo depois e incluindo a terceira barra.
Os cabeçalhos também não estão longe. Eles estão em seu próprio objeto na solicitação chamado headers
.
const { headers } = request
const userAgent = headers['user-agent']
É importante notar aqui que todos os cabeçalhos são representados apenas em minúsculas, independentemente de como o cliente realmente os enviou. Isso simplifica a tarefa de analisar cabeçalhos para qualquer finalidade.
Se alguns cabeçalhos são repetidos, seus valores são sobrescritos ou unidos como strings separadas por vírgulas, dependendo do cabeçalho. Em alguns casos, isso pode ser problemático, então rawHeaders
também está disponível.
Corpo da Solicitação
Ao receber uma solicitação POST ou PUT, o corpo da solicitação pode ser importante para sua aplicação. Obter os dados do corpo é um pouco mais complexo do que acessar os cabeçalhos da solicitação. O objeto de solicitação que é passado para um manipulador implementa a interface ReadableStream
. Este fluxo pode ser escutado ou canalizado para outro lugar, assim como qualquer outro fluxo. Podemos pegar os dados diretamente do fluxo ouvindo os eventos 'data'
e 'end'
do fluxo.
O bloco emitido em cada evento 'data'
é um Buffer
. Se você sabe que serão dados de string, o melhor a fazer é coletar os dados em um array e, no 'end'
, concatená-los e convertê-los em string.
let body = []
request.on('data', chunk => {
body.push(chunk)
})
request.on('end', () => {
body = Buffer.concat(body).toString()
// neste ponto, 'body' tem todo o corpo da solicitação armazenado nele como uma string
})
NOTA
Isso pode parecer um pouco tedioso, e em muitos casos, é. Felizmente, existem módulos como concat-stream
e body
no npm que podem ajudar a esconder parte dessa lógica. É importante ter um bom entendimento do que está acontecendo antes de seguir por esse caminho, e é por isso que você está aqui!
Uma Coisa Rápida Sobre Erros
Como o objeto de solicitação é um ReadableStream
, ele também é um EventEmitter
e se comporta como tal quando ocorre um erro.
Um erro no fluxo de solicitação se apresenta emitindo um evento 'error'
no fluxo. Se você não tiver um ouvinte para esse evento, o erro será lançado, o que pode travar seu programa Node.js. Portanto, você deve adicionar um ouvinte 'error'
em seus fluxos de solicitação, mesmo que apenas o registre e continue seu caminho. (Embora provavelmente seja melhor enviar algum tipo de resposta de erro HTTP. Mais sobre isso mais tarde.)
request.on('error', err => {
// Isso imprime a mensagem de erro e o rastreamento de pilha para stderr.
console.error(err.stack)
})
Existem outras maneiras de lidar com esses erros, como outras abstrações e ferramentas, mas esteja sempre ciente de que erros podem e acontecem, e você terá que lidar com eles.
O Que Temos Até Agora
Neste ponto, já abordamos a criação de um servidor e a captura do método, URL, cabeçalhos e corpo das solicitações. Juntando tudo, pode ficar parecido com isto:
const http = require('node:http');
http.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request.on('error', err => console.error(err));
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
// Neste ponto, temos os cabeçalhos, método, url e corpo, e podemos agora
// fazer o que for necessário para responder a esta solicitação.
});
});
.listen(8080); // Ativa este servidor, escutando na porta 8080.
Se executarmos este exemplo, seremos capazes de receber solicitações, mas não de responder a elas. De facto, se aceder a este exemplo num navegador web, a sua solicitação irá expirar por tempo de espera, pois nada está a ser enviado de volta para o cliente.
Até agora não tocamos no objecto de resposta, que é uma instância de ServerResponse
, que é um WritableStream
. Ele contém muitos métodos úteis para enviar dados de volta para o cliente. Vamos abordar isso a seguir.
Código de Estado HTTP
Se não se der ao trabalho de defini-lo, o código de estado HTTP numa resposta será sempre 200. Claro que nem todas as respostas HTTP justificam isto, e em algum momento certamente quererá enviar um código de estado diferente. Para fazer isso, pode definir a propriedade statusCode
.
response.statusCode = 404 // Informa o cliente que o recurso não foi encontrado.
Existem alguns atalhos para isto, como veremos em breve.
Definindo Cabeçalhos de Resposta
Os cabeçalhos são definidos através de um método conveniente chamado setHeader
.
response.setHeader('Content-Type', 'application/json')
response.setHeader('X-Powered-By', 'bacon')
Ao definir os cabeçalhos numa resposta, o caso é insensível aos seus nomes. Se definir um cabeçalho repetidamente, o último valor que definir é o valor que será enviado.
Enviando Dados de Cabeçalho Explicitamente
Os métodos de configuração dos cabeçalhos e do código de status que já discutimos assumem que você está usando "cabeçalhos implícitos". Isso significa que você está contando com o nó para enviar os cabeçalhos para você no momento correto antes de começar a enviar dados do corpo.
Se desejar, você pode escrever explicitamente os cabeçalhos para o fluxo de resposta. Para fazer isso, existe um método chamado writeHead
, que escreve o código de status e os cabeçalhos para o fluxo.
Enviando Dados de Cabeçalho Explicitamente
response.writeHead(200, {
'Content-Type': 'application/json',
'X-Powered-By': 'bacon',
})
Depois de definir os cabeçalhos (implicitamente ou explicitamente), você está pronto para começar a enviar dados de resposta.
Enviando o Corpo da Resposta
Como o objeto de resposta é um WritableStream
, escrever um corpo de resposta para o cliente é apenas uma questão de usar os métodos de fluxo habituais.
response.write('<html>')
response.write('<body>')
response.write('<h1>Hello, World!</h1>')
response.write('</body>')
response.write('</html>')
response.end()
A função end
em streams também pode receber alguns dados opcionais para enviar como o último bit de dados no stream, então podemos simplificar o exemplo acima da seguinte forma.
response.end('<html><body><h1>hello,world!</h1></body></html>')
NOTA
É importante definir o status e os cabeçalhos antes de começar a escrever blocos de dados para o corpo. Isso faz sentido, pois os cabeçalhos vêm antes do corpo nas respostas HTTP.
Outra Coisa Rápida Sobre Erros
O fluxo de resposta também pode emitir eventos 'error', e em algum momento você terá que lidar com isso também. Todos os conselhos para erros de fluxo de solicitação ainda se aplicam aqui.
Juntando Tudo
Agora que aprendemos sobre como fazer respostas HTTP, vamos juntar tudo. Com base no exemplo anterior, vamos criar um servidor que envia de volta todos os dados que foram enviados para nós pelo usuário. Vamos formatar esses dados como JSON usando JSON.stringify
.
const http = require('node:http')
http
.createServer((request, response) => {
const { headers, method, url } = request
let body = []
request
.on('error', err => {
console.error(err)
})
.on('data', chunk => {
body.push(chunk)
})
.on('end', () => {
body = Buffer.concat(body).toString()
// INÍCIO DE COISAS NOVAS
response.on('error', err => {
console.error(err)
})
response.statusCode = 200
response.setHeader('Content-Type', 'application/json')
// Nota: as 2 linhas acima poderiam ser substituídas por esta próxima:
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body }
response.write(JSON.stringify(responseBody))
response.end()
// Nota: as 2 linhas acima poderiam ser substituídas por esta próxima:
// response.end(JSON.stringify(responseBody))
// FIM DE COISAS NOVAS
})
})
.listen(8080)
Exemplo de EchoServer
Vamos simplificar o exemplo anterior para criar um servidor de eco simples, que apenas envia de volta na resposta quaisquer dados recebidos na solicitação. Tudo o que precisamos fazer é pegar os dados do fluxo de solicitação e escrever esses dados no fluxo de resposta, semelhante ao que fizemos anteriormente.
const http = require('node:http');
http.createServer((request, response) => {
let body = [];
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
response.end(body);
});
});
.listen(8080);
Agora vamos ajustar isso. Queremos enviar um eco apenas nas seguintes condições:
- O método da solicitação é POST.
- A URL é /echo.
Em qualquer outro caso, queremos simplesmente responder com um 404.
const http = require('node:http')
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
let body = []
request
.on('data', chunk => {
body.push(chunk)
})
.on('end', () => {
body = Buffer.concat(body).toString()
response.end(body)
})
} else {
response.statusCode = 404
response.end()
}
})
.listen(8080)
NOTA
Ao verificar a URL dessa maneira, estamos fazendo uma forma de "roteamento". Outras formas de roteamento podem ser tão simples quanto instruções switch
ou tão complexas quanto frameworks inteiros como express
. Se você está procurando algo que faça roteamento e nada mais, tente router
.
Ótimo! Agora vamos tentar simplificar isso. Lembre-se, o objeto de solicitação é um ReadableStream
e o objeto de resposta é um WritableStream
. Isso significa que podemos usar pipe
para direcionar dados de um para o outro. É exatamente o que queremos para um servidor de eco!
const http = require('node:http')
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response)
} else {
response.statusCode = 404
response.end()
}
})
.listen(8080)
Fluxos uhu!
Ainda não terminamos. Como mencionado várias vezes neste guia, erros podem e acontecem, e precisamos lidar com eles.
Para lidar com erros no fluxo de solicitação, registraremos o erro em stderr
e enviaremos um código de status 400 para indicar uma Solicitação Inválida
. Em um aplicativo do mundo real, no entanto, queremos inspecionar o erro para descobrir qual seria o código de status e a mensagem corretos. Como de costume com erros, você deve consultar a documentação de Erros.
Na resposta, apenas registraremos o erro em stderr
.
const http = require('node:http')
http
.createServer((request, response) => {
request.on('error', err => {
console.error(err)
response.statusCode = 400
response.end()
})
response.on('error', err => {
console.error(err)
})
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response)
} else {
response.statusCode = 404
response.end()
}
})
.listen(8080)
Agora abordamos a maioria dos conceitos básicos do tratamento de solicitações HTTP. Neste ponto, você deve ser capaz de:
- Instanciar um servidor HTTP com uma função de manipulador de
solicitação
e fazê-lo escutar em uma porta. - Obter cabeçalhos, URL, método e dados do corpo de objetos
solicitação
. - Tomar decisões de roteamento com base na URL e/ou outros dados em objetos
solicitação
. - Enviar cabeçalhos, códigos de status HTTP e dados do corpo por meio de objetos
resposta
. - Canalizar dados de objetos
solicitação
e para objetos de resposta. - Lidar com erros de fluxo nos fluxos
solicitação
eresposta
.
A partir desses conceitos básicos, os servidores HTTP Node.js para muitos casos de uso típicos podem ser construídos. Existem muitas outras coisas que essas APIs fornecem, portanto, certifique-se de ler os documentos da API para EventEmitters
, Streams
e HTTP
.