Anatomía de una Transacción HTTP
El propósito de esta guía es impartir una sólida comprensión del proceso de manejo de HTTP en Node.js. Asumiremos que usted sabe, en un sentido general, cómo funcionan las solicitudes HTTP, independientemente del lenguaje o entorno de programación. También asumiremos cierta familiaridad con los EventEmitters y Streams de Node.js. Si no está muy familiarizado con ellos, vale la pena leer rápidamente la documentación de la API para cada uno de ellos.
Crear el Servidor
Cualquier aplicación de servidor web de node en algún momento tendrá que crear un objeto de servidor web. Esto se hace usando createServer
.
const http = require('node:http');
const server = http.createServer((request, response) => {
// ¡aquí sucede la magia!
});
La función que se pasa a createServer
se llama una vez por cada solicitud HTTP que se realiza contra ese servidor, por lo que se llama el manejador de solicitudes. De hecho, el objeto Server devuelto por createServer
es un EventEmitter, y lo que tenemos aquí es solo una abreviatura para crear un objeto de servidor y luego agregar el listener más tarde.
const server = http.createServer();
server.on('request', (request, response) => {
// ¡el mismo tipo de magia sucede aquí!
});
Cuando una solicitud HTTP llega al servidor, node llama a la función de manejador de solicitudes con algunos objetos útiles para lidiar con la transacción, la solicitud y la respuesta. Llegaremos a ellos en breve. Para realmente atender las solicitudes, el método listen
debe ser llamado en el objeto del servidor. En la mayoría de los casos, todo lo que necesitará pasar a listen
es el número de puerto en el que desea que el servidor escuche. También hay algunas otras opciones, así que consulte la referencia de la API.
Método, URL y Cabeceras
Al manejar una solicitud, lo primero que probablemente querrá hacer es observar el método y la URL, para que se puedan tomar las acciones apropiadas. Node.js hace esto relativamente sencillo al colocar propiedades útiles en el objeto de solicitud.
const { method, url } = request;
El objeto de solicitud es una instancia de IncomingMessage
. El método aquí siempre será un método/verbo HTTP normal. La URL es la URL completa sin el servidor, protocolo o puerto. Para una URL típica, esto significa todo después e incluyendo la tercera barra inclinada.
Las cabeceras tampoco están lejos. Están en su propio objeto en la solicitud llamado headers
.
const { headers } = request;
const userAgent = headers['user-agent'];
Es importante tener en cuenta aquí que todas las cabeceras se representan solo en minúsculas, independientemente de cómo el cliente las haya enviado realmente. Esto simplifica la tarea de analizar las cabeceras para cualquier propósito.
Si algunas cabeceras se repiten, entonces sus valores se sobrescriben o se unen como cadenas separadas por comas, dependiendo de la cabecera. En algunos casos, esto puede ser problemático, por lo que rawHeaders
también está disponible.
Cuerpo de la Solicitud
Al recibir una solicitud POST o PUT, el cuerpo de la solicitud podría ser importante para tu aplicación. Acceder a los datos del cuerpo es un poco más complejo que acceder a los encabezados de la solicitud. El objeto de solicitud que se pasa a un manejador implementa la interfaz ReadableStream
. Este flujo se puede escuchar o canalizar a otra parte al igual que cualquier otro flujo. Podemos obtener los datos directamente del flujo escuchando los eventos 'data'
y 'end'
del flujo.
El fragmento emitido en cada evento 'data'
es un Buffer
. Si sabes que serán datos de cadena, lo mejor es recopilar los datos en una matriz y luego, en el 'end'
, concatenarlos y convertirlos en cadena.
let body = [];
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
// en este punto, 'body' tiene todo el cuerpo de la solicitud almacenado en él como una cadena
});
NOTA
Esto puede parecer un poco tedioso y, en muchos casos, lo es. Afortunadamente, existen módulos como concat-stream
y body
en npm que pueden ayudar a ocultar parte de esta lógica. ¡Es importante tener una buena comprensión de lo que está sucediendo antes de tomar ese camino, y es por eso que estás aquí!
Una Cosa Rápida Sobre los Errores
Dado que el objeto de solicitud es un ReadableStream
, también es un EventEmitter
y se comporta como tal cuando ocurre un error.
Un error en el flujo de solicitud se presenta emitiendo un evento 'error'
en el flujo. Si no tienes un escuchador para ese evento, el error se lanzará, lo que podría bloquear tu programa Node.js. Por lo tanto, debes agregar un escuchador 'error'
en tus flujos de solicitud, incluso si solo lo registras y continúas tu camino. (Aunque probablemente sea mejor enviar algún tipo de respuesta de error HTTP. Más sobre eso más adelante).
request.on('error', err => {
// Esto imprime el mensaje de error y el seguimiento de la pila en stderr.
console.error(err.stack);
});
Hay otras formas de manejar estos errores, como otras abstracciones y herramientas, pero siempre debes ser consciente de que los errores pueden ocurrir y ocurren, y tendrás que lidiar con ellos.
Lo que tenemos hasta ahora
En este punto, hemos cubierto la creación de un servidor y la obtención del método, la URL, los encabezados y el cuerpo de las solicitudes. Cuando juntamos todo eso, podría verse algo así:
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();
// En este punto, tenemos los encabezados, el método, la URL y el cuerpo, y ahora podemos
// hacer lo que necesitemos para responder a esta solicitud.
});
});
.listen(8080); // Activa este servidor, escuchando en el puerto 8080.
Si ejecutamos este ejemplo, podremos recibir solicitudes, pero no responder a ellas. De hecho, si accedes a este ejemplo en un navegador web, tu solicitud se agotará, ya que no se envía nada de vuelta al cliente.
Hasta ahora no hemos tocado el objeto de respuesta en absoluto, que es una instancia de ServerResponse
, que es un WritableStream
. Contiene muchos métodos útiles para enviar datos de vuelta al cliente. Lo cubriremos a continuación.
Código de estado HTTP
Si no te molestas en configurarlo, el código de estado HTTP en una respuesta siempre será 200. Por supuesto, no todas las respuestas HTTP lo justifican, y en algún momento definitivamente querrás enviar un código de estado diferente. Para ello, puedes establecer la propiedad statusCode
.
response.statusCode = 404; // Dile al cliente que no se encontró el recurso.
Hay algunos otros atajos para esto, como veremos pronto.
Establecer encabezados de respuesta
Los encabezados se configuran a través de un método conveniente llamado setHeader
.
response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');
Al configurar los encabezados en una respuesta, las mayúsculas y minúsculas no importan en sus nombres. Si configuras un encabezado repetidamente, el último valor que establezcas es el valor que se envía.
Envío explícito de datos de encabezado
Los métodos para establecer los encabezados y el código de estado que ya hemos discutido asumen que estás utilizando "encabezados implícitos". Esto significa que estás contando con que Node envíe los encabezados por ti en el momento correcto antes de que comiences a enviar datos del cuerpo.
Si lo deseas, puedes escribir explícitamente los encabezados en el flujo de respuesta. Para hacer esto, hay un método llamado writeHead
, que escribe el código de estado y los encabezados en el flujo.
Envío explícito de datos de encabezado
response.writeHead(200, {
'Content-Type': 'application/json',
'X-Powered-By': 'bacon',
});
Una vez que hayas establecido los encabezados (ya sea implícita o explícitamente), estás listo para comenzar a enviar datos de respuesta.
Envío del cuerpo de la respuesta
Dado que el objeto de respuesta es un WritableStream
, escribir un cuerpo de respuesta al cliente es solo una cuestión de usar los métodos de flujo habituales.
response.write('<html>');
response.write('<body>');
response.write('<h1>¡Hola, Mundo!</h1>');
response.write('</body>');
response.write('</html>');
response.end();
La función end
en los flujos también puede recibir algunos datos opcionales para enviar como el último fragmento de datos en el flujo, por lo que podemos simplificar el ejemplo anterior de la siguiente manera.
response.end('<html><body><h1>¡hola, mundo!</h1></body></html>');
NOTE
Es importante establecer el estado y los encabezados antes de comenzar a escribir fragmentos de datos en el cuerpo. Esto tiene sentido, ya que los encabezados van antes del cuerpo en las respuestas HTTP.
Otra cosa rápida sobre errores
El flujo de respuesta también puede emitir eventos de 'error', y en algún momento tendrás que lidiar con eso también. Todos los consejos para los errores del flujo de solicitud todavía se aplican aquí.
Juntándolo todo
Ahora que hemos aprendido sobre cómo crear respuestas HTTP, vamos a juntarlo todo. Basándonos en el ejemplo anterior, vamos a crear un servidor que devuelva todos los datos que nos envió el usuario. Formatearemos esos datos 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();
// COMIENZO DE COSAS NUEVAS
response.on('error', err => {
console.error(err);
});
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
// Nota: las 2 líneas anteriores podrían reemplazarse con la siguiente:
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body };
response.write(JSON.stringify(responseBody));
response.end();
// Nota: las 2 líneas anteriores podrían reemplazarse con la siguiente:
// response.end(JSON.stringify(responseBody))
// FIN DE COSAS NUEVAS
});
})
.listen(8080);
Ejemplo de EchoServer
Vamos a simplificar el ejemplo anterior para crear un servidor eco simple, que simplemente envía los datos que recibe en la solicitud de vuelta en la respuesta. Todo lo que necesitamos hacer es tomar los datos del flujo de solicitud y escribir esos datos en el flujo de respuesta, de manera similar a lo que hicimos 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);
Ahora vamos a modificar esto. Queremos enviar un eco solo bajo las siguientes condiciones:
- El método de solicitud es POST.
- La URL es /echo.
En cualquier otro caso, queremos simplemente responder con un 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);
NOTE
Al verificar la URL de esta manera, estamos haciendo una forma de "enrutamiento". Otras formas de enrutamiento pueden ser tan simples como sentencias switch
o tan complejas como frameworks completos como express
. Si estás buscando algo que haga enrutamiento y nada más, prueba router
.
¡Genial! Ahora vamos a intentar simplificar esto. Recuerda, el objeto de solicitud es un ReadableStream
y el objeto de respuesta es un WritableStream
. Eso significa que podemos usar pipe
para dirigir los datos de uno a otro. ¡Eso es exactamente lo que queremos para un servidor 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);
¡Vivan los streams!
Sin embargo, aún no hemos terminado. Como se mencionó varias veces en esta guía, los errores pueden ocurrir y ocurren, y debemos lidiar con ellos.
Para manejar los errores en el flujo de solicitud, registraremos el error en stderr
y enviaremos un código de estado 400 para indicar una Bad Request
. Sin embargo, en una aplicación del mundo real, querríamos inspeccionar el error para averiguar cuál sería el código de estado y el mensaje correctos. Como de costumbre con los errores, debes consultar la documentación de Error.
En la respuesta, simplemente registraremos el error en 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);
Ahora hemos cubierto la mayoría de los conceptos básicos del manejo de solicitudes HTTP. En este punto, deberías ser capaz de:
- Instanciar un servidor HTTP con una función de controlador de
request
, y hacer que escuche en un puerto. - Obtener encabezados, URL, método y datos del cuerpo de los objetos
request
. - Tomar decisiones de enrutamiento basadas en la URL y/u otros datos en los objetos
request
. - Enviar encabezados, códigos de estado HTTP y datos del cuerpo a través de objetos
response
. - Dirigir datos desde objetos
request
y hacia objetos response. - Manejar errores de flujo tanto en los flujos
request
comoresponse
.
A partir de estos conceptos básicos, se pueden construir servidores HTTP de Node.js para muchos casos de uso típicos. Hay muchas otras cosas que proporcionan estas API, así que asegúrate de leer la documentación de la API para EventEmitters
, Streams
y HTTP
.