Skip to content

Anatomía de una Transacción HTTP

El propósito de esta guía es impartir una comprensión sólida del proceso de manejo de HTTP en Node.js. Asumiremos que sabes, en un sentido general, cómo funcionan las peticiones 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ás del todo familiarizado con ellos, vale la pena leer rápidamente la documentación de la API de 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.

javascript
const http = require('node:http')
const server = http.createServer((request, response) => {
  // ¡aquí ocurre la magia!
})

La función que se pasa a createServer se llama una vez por cada petición HTTP que se hace contra ese servidor, por lo que se llama el manejador de peticiones. De hecho, el objeto Servidor 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.

javascript
const server = http.createServer()
server.on('request', (request, response) => {
  // ¡aquí ocurre el mismo tipo de magia!
})

Cuando una petición HTTP llega al servidor, node llama a la función manejadora de peticiones con algunos objetos útiles para manejar la transacción, la petición y la respuesta. Llegaremos a ellos en breve. Para realmente servir peticiones, el método listen debe ser llamado en el objeto del servidor. En la mayoría de los casos, todo lo que necesitarás pasar a listen es el número de puerto en el que quieres que el servidor escuche. También hay algunas otras opciones, así que consulta la referencia de la API.

Método, URL y Encabezados

Al manejar una solicitud, lo primero que probablemente quieras hacer es mirar el método y la URL, para que se puedan tomar las acciones apropiadas. Node.js hace esto relativamente fácil al colocar propiedades útiles en el objeto de solicitud.

javascript
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, el protocolo o el puerto. Para una URL típica, esto significa todo después e incluyendo la tercera barra diagonal.

Los encabezados tampoco están muy lejos. Están en su propio objeto en la solicitud llamado headers.

javascript
const { headers } = request
const userAgent = headers['user-agent']

Es importante tener en cuenta aquí que todos los encabezados se representan solo en minúsculas, independientemente de cómo el cliente los haya enviado realmente. Esto simplifica la tarea de analizar los encabezados para cualquier propósito.

Si algunos encabezados se repiten, entonces sus valores se sobrescriben o se unen como cadenas separadas por comas, dependiendo del encabezado. 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 complicado que acceder a los encabezados de la solicitud. El objeto de solicitud que se pasa a un controlador implementa la interfaz ReadableStream. Se puede escuchar o canalizar este flujo a otro lugar como 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 un arreglo, luego en el evento 'end', concatenarlos y convertirlos en cadena.

javascript
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 como una cadena
})

NOTA

Esto puede parecer un poco tedioso y, en muchos casos, lo es. Afortunadamente, hay 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 seguir ese camino, y es por eso que estás aquí!

Un Apunte Rápido Sobre 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 listener para ese evento, el error será lanzado, lo que podría hacer que tu programa Node.js se cuelgue. Por lo tanto, deberías añadir un listener '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.)

javascript
request.on('error', err => {
  // Esto imprime el mensaje de error y el stack trace en stderr.
  console.error(err.stack)
})

Hay otras formas de manejar estos errores como otras abstracciones y herramientas, pero siempre ten en cuenta que los errores pueden ocurrir y ocurren, y vas a tener 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, las cabeceras y el cuerpo de las solicitudes. Cuando juntamos todo eso, podría verse algo así:

javascript
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 las cabeceras, 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ía, 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 justifican esto, y en algún momento definitivamente querrás enviar un código de estado diferente. Para hacer eso, puedes establecer la propiedad statusCode.

javascript
response.statusCode = 404 // Indica al cliente que el recurso no se encontró.

Hay algunos otros atajos para esto, como veremos pronto.

Establecer encabezados de respuesta

Los encabezados se establecen a través de un método conveniente llamado setHeader.

javascript
response.setHeader('Content-Type', 'application/json')
response.setHeader('X-Powered-By', 'bacon')

Al establecer los encabezados en una respuesta, las mayúsculas y minúsculas no son importantes en sus nombres. Si estableces un encabezado repetidamente, el último valor que establezcas es el valor que se envía.

Enviar explícitamente 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 empieces a enviar datos de cuerpo.

Si quieres, puedes escribir explícitamente los encabezados en el flujo de respuesta. Para hacer esto, existe 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

javascript
response.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'bacon',
})

Una vez que haya establecido los encabezados (ya sea implícita o explícitamente), estará 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.

javascript
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 tomar algunos datos opcionales para enviar como el último bit de datos en el flujo, por lo que podemos simplificar el ejemplo anterior de la siguiente manera.

javascript
response.end('<html><body><h1>¡hola, mundo!</h1></body></html>')

NOTA

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 que el cuerpo en las respuestas HTTP.

Otra cosa rápida sobre los errores

El flujo de respuesta también puede emitir eventos 'error', y en algún momento tendrás que lidiar con eso también. Todos los consejos para los errores del flujo de solicitud siguen siendo válidos aquí.

Juntarlo todo

Ahora que hemos aprendido sobre cómo hacer respuestas HTTP, juntemos todo. Basándonos en el ejemplo anterior, vamos a hacer un servidor que envíe de vuelta todos los datos que nos envió el usuario. Formatearemos esos datos como JSON usando JSON.stringify.

javascript
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 LAS 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 ser reemplazadas por esta 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 ser reemplazadas por esta siguiente:
        // response.end(JSON.stringify(responseBody))
        // FIN DE LAS COSAS NUEVAS
      })
  })
  .listen(8080)

Ejemplo de EchoServer

Simplifiquemos el ejemplo anterior para crear un servidor de eco simple, que simplemente envía los datos recibidos en la solicitud de vuelta en la respuesta. Todo lo que necesitamos hacer es tomar los datos del flujo de la solicitud y escribir esos datos en el flujo de la respuesta, de manera similar a lo que hicimos anteriormente.

javascript
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 la solicitud es POST.
  • La URL es /echo.

En cualquier otro caso, queremos simplemente responder con un 404.

javascript
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

Al verificar la URL de esta manera, estamos haciendo una forma de "enrutamiento". Otras formas de enrutamiento pueden ser tan simples como declaraciones 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 al otro. ¡Eso es exactamente lo que queremos para un servidor de eco!

javascript
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, todavía no hemos terminado. Como se ha mencionado 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 la solicitud, registraremos el error en stderr y enviaremos un código de estado 400 para indicar una Solicitud Incorrecta. 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 es habitual con los errores, deberías consultar la documentación de Errores.

En la respuesta, solo registraremos el error en stderr.

javascript
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 de cuerpo de objetos request.
  • Tomar decisiones de enrutamiento basadas en la URL y/o otros datos en objetos request.
  • Enviar encabezados, códigos de estado HTTP y datos de cuerpo a través de objetos response.
  • Redirigir datos desde objetos request a objetos response.
  • Manejar errores de flujo tanto en los flujos de request como de response.

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.