Skip to content

Anatomie d'une transaction HTTP

Le but de ce guide est de fournir une compréhension solide du processus de gestion HTTP de Node.js. Nous supposerons que vous savez, de manière générale, comment fonctionnent les requêtes HTTP, quel que soit le langage ou l'environnement de programmation. Nous supposerons également une certaine familiarité avec les EventEmitters et les Streams de Node.js. Si vous n'êtes pas tout à fait familier avec eux, il vaut la peine de lire rapidement la documentation de l'API pour chacun d'eux.

Créer le serveur

Toute application de serveur web Node devra à un moment donné créer un objet serveur web. Ceci est fait en utilisant createServer.

javascript
const http = require('node:http')
const server = http.createServer((request, response) => {
  // la magie opère ici !
})

La fonction passée à createServer est appelée une fois pour chaque requête HTTP effectuée sur ce serveur, elle est donc appelée gestionnaire de requête. En fait, l'objet Server retourné par createServer est un EventEmitter, et ce que nous avons ici est juste un raccourci pour créer un objet serveur puis ajouter l'écouteur plus tard.

javascript
const server = http.createServer()
server.on('request', (request, response) => {
  // le même type de magie opère ici !
})

Lorsqu'une requête HTTP atteint le serveur, Node appelle la fonction de gestionnaire de requête avec quelques objets pratiques pour gérer la transaction, la requête et la réponse. Nous y reviendrons bientôt. Pour réellement servir les requêtes, la méthode listen doit être appelée sur l'objet serveur. Dans la plupart des cas, tout ce dont vous aurez besoin pour passer à listen est le numéro de port sur lequel vous voulez que le serveur écoute. Il existe également d'autres options, consultez donc la référence de l'API.

Méthode, URL et en-têtes

Lors de la gestion d'une requête, la première chose que vous voudrez probablement faire est de regarder la méthode et l'URL, afin que des actions appropriées puissent être prises. Node.js rend cela relativement simple en plaçant des propriétés pratiques sur l'objet requête.

javascript
const { method, url } = request

L'objet requête est une instance de IncomingMessage. La méthode ici sera toujours une méthode/verbe HTTP normale. L'URL est l'URL complète sans le serveur, le protocole ou le port. Pour une URL typique, cela signifie tout après et y compris la troisième barre oblique.

Les en-têtes ne sont pas loin non plus. Ils sont dans leur propre objet sur la requête appelé headers.

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

Il est important de noter ici que tous les en-têtes sont représentés en minuscules uniquement, indépendamment de la façon dont le client les a effectivement envoyés. Cela simplifie la tâche d'analyse des en-têtes pour quelque but que ce soit.

Si certains en-têtes sont répétés, leurs valeurs sont alors écrasées ou jointes ensemble sous forme de chaînes de caractères séparées par des virgules, selon l'en-tête. Dans certains cas, cela peut être problématique, donc rawHeaders est également disponible.

Corps de la requête

Lors de la réception d'une requête POST ou PUT, le corps de la requête peut être important pour votre application. Accéder aux données du corps est un peu plus complexe que d'accéder aux en-têtes de la requête. L'objet requête passé à un gestionnaire implémente l'interface ReadableStream. Ce flux peut être écouté ou redirigé ailleurs comme n'importe quel autre flux. Nous pouvons récupérer les données directement du flux en écoutant les événements 'data' et 'end' du flux.

Le fragment émis à chaque événement 'data' est un Buffer. Si vous savez qu'il s'agit de données de type chaîne de caractères, la meilleure chose à faire est de collecter les données dans un tableau, puis, à la fin ('end'), de les concaténer et de les convertir en chaîne de caractères.

javascript
let body = []
request.on('data', chunk => {
  body.push(chunk)
})
request.on('end', () => {
  body = Buffer.concat(body).toString()
  // à ce stade, 'body' contient tout le corps de la requête stocké sous forme de chaîne de caractères
})

NOTE

Cela peut sembler un peu fastidieux, et dans de nombreux cas, c'est le cas. Heureusement, il existe des modules comme concat-stream et body sur npm qui peuvent aider à masquer une partie de cette logique. Il est important de bien comprendre ce qui se passe avant d'emprunter cette voie, et c'est pourquoi vous êtes là !

Un petit mot sur les erreurs

Puisque l'objet requête est un ReadableStream, c'est aussi un EventEmitter et se comporte comme tel lorsqu'une erreur se produit.

Une erreur dans le flux de requête se présente en émettant un événement 'error' sur le flux. Si vous n'avez pas d'écouteur pour cet événement, l'erreur sera levée, ce qui pourrait planter votre programme Node.js. Vous devez donc ajouter un écouteur 'error' sur vos flux de requête, même si vous vous contentez de le journaliser et de continuer. (Bien qu'il soit probablement préférable d'envoyer une sorte de réponse d'erreur HTTP. Plus de détails à ce sujet plus tard.)

javascript
request.on('error', err => {
  // Ceci imprime le message d'erreur et la trace de la pile sur stderr.
  console.error(err.stack)
})

Il existe d'autres façons de gérer ces erreurs telles que d'autres abstractions et outils, mais soyez toujours conscients que des erreurs peuvent se produire et se produisent, et vous devrez les gérer.

Ce que nous avons jusqu'à présent

À ce stade, nous avons abordé la création d'un serveur et la récupération de la méthode, de l'URL, des en-têtes et du corps des requêtes. Lorsque nous rassemblons tout cela, cela pourrait ressembler à ceci :

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();
        // À ce stade, nous avons les en-têtes, la méthode, l'URL et le corps, et pouvons maintenant
        // faire ce que nous devons pour répondre à cette requête.
    });
});

.listen(8080); // Active ce serveur, à l'écoute du port 8080.

Si nous exécutons cet exemple, nous pourrons recevoir des requêtes, mais pas y répondre. En fait, si vous accédez à cet exemple dans un navigateur web, votre requête serait expirée, car rien n'est renvoyé au client.

Jusqu'à présent, nous n'avons pas du tout abordé l'objet de réponse, qui est une instance de ServerResponse, qui est un WritableStream. Il contient de nombreuses méthodes utiles pour renvoyer des données au client. Nous aborderons cela ensuite.

Code de statut HTTP

Si vous ne vous en souciez pas, le code de statut HTTP d'une réponse sera toujours 200. Bien sûr, toutes les réponses HTTP ne le justifient pas, et à un moment donné, vous voudrez certainement envoyer un code de statut différent. Pour ce faire, vous pouvez définir la propriété statusCode.

javascript
response.statusCode = 404 // Informe le client que la ressource n'a pas été trouvée.

Il existe d'autres raccourcis à cela, comme nous le verrons bientôt.

Définition des en-têtes de réponse

Les en-têtes sont définis via une méthode pratique appelée setHeader.

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

Lors de la définition des en-têtes d'une réponse, la casse est insensible à leurs noms. Si vous définissez un en-tête plusieurs fois, la dernière valeur que vous définissez est la valeur qui est envoyée.

Envoi explicite des données d'en-tête

Les méthodes de définition des en-têtes et du code de statut que nous avons déjà abordées supposent que vous utilisez des "en-têtes implicites". Cela signifie que vous comptez sur Node pour envoyer les en-têtes au bon moment avant de commencer à envoyer les données du corps.

Si vous le souhaitez, vous pouvez écrire explicitement les en-têtes dans le flux de réponse. Pour ce faire, il existe une méthode appelée writeHead, qui écrit le code de statut et les en-têtes dans le flux.

Envoi explicite des données d'en-tête

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

Une fois que vous avez défini les en-têtes (implicitement ou explicitement), vous êtes prêt à commencer à envoyer les données de réponse.

Envoi du corps de la réponse

Étant donné que l'objet de réponse est un WritableStream, l'écriture d'un corps de réponse vers le client est simplement une question d'utilisation des méthodes de flux habituelles.

javascript
response.write('<html>')
response.write('<body>')
response.write('<h1>Hello, World!</h1>')
response.write('</body>')
response.write('</html>')
response.end()

La fonction end sur les flux peut également prendre des données facultatives à envoyer comme dernier morceau de données sur le flux, nous pouvons donc simplifier l'exemple ci-dessus comme suit.

javascript
response.end('<html><body><h1>hello,world!</h1></body></html>')

NOTE

Il est important de définir le statut et les en-têtes avant de commencer à écrire des blocs de données dans le corps. Cela est logique, car les en-têtes viennent avant le corps dans les réponses HTTP.

Autre détail rapide sur les erreurs

Le flux de réponse peut également émettre des événements 'error', et à un moment donné, vous devrez également gérer cela. Tous les conseils concernant les erreurs de flux de requête s'appliquent toujours ici.

Mettre tout cela ensemble

Maintenant que nous avons appris à créer des réponses HTTP, rassemblons tout cela. En nous appuyant sur l'exemple précédent, nous allons créer un serveur qui renvoie toutes les données qui nous ont été envoyées par l'utilisateur. Nous allons formater ces données au format JSON en utilisant 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()
        // DÉBUT DES NOUVELLES INFORMATIONS
        response.on('error', err => {
          console.error(err)
        })
        response.statusCode = 200
        response.setHeader('Content-Type', 'application/json')
        // Remarque : les 2 lignes ci-dessus pourraient être remplacées par la suivante :
        // response.writeHead(200, {'Content-Type': 'application/json'})
        const responseBody = { headers, method, url, body }
        response.write(JSON.stringify(responseBody))
        response.end()
        // Remarque : les 2 lignes ci-dessus pourraient être remplacées par la suivante :
        // response.end(JSON.stringify(responseBody))
        // FIN DES NOUVELLES INFORMATIONS
      })
  })
  .listen(8080)

Exemple d'EchoServer

Simplifions l'exemple précédent pour créer un simple serveur echo, qui renvoie simplement les données reçues dans la requête dans la réponse. Tout ce que nous devons faire est de récupérer les données du flux de requête et d'écrire ces données dans le flux de réponse, de manière similaire à ce que nous avons fait précédemment.

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);

Modifions cela. Nous voulons uniquement envoyer un écho dans les conditions suivantes :

  • La méthode de requête est POST.
  • L'URL est /echo.

Dans tous les autres cas, nous voulons simplement répondre avec 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)

NOTE

En vérifiant l'URL de cette manière, nous effectuons une forme de « routage ». D'autres formes de routage peuvent être aussi simples que des instructions switch ou aussi complexes que des frameworks entiers comme express. Si vous recherchez quelque chose qui effectue le routage et rien d'autre, essayez router.

Génial ! Maintenant, essayons de simplifier cela. N'oubliez pas que l'objet request est un ReadableStream et que l'objet response est un WritableStream. Cela signifie que nous pouvons utiliser pipe pour diriger les données de l'un à l'autre. C'est exactement ce que nous voulons pour un serveur echo !

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)

Vive les flux !

Nous n'avons pas encore tout à fait terminé. Comme mentionné plusieurs fois dans ce guide, des erreurs peuvent survenir et surviennent, et nous devons les gérer.

Pour gérer les erreurs sur le flux de requête, nous allons consigner l'erreur dans stderr et envoyer un code d'état 400 pour indiquer une Mauvaise requête. Dans une application réelle, cependant, nous voudrions inspecter l'erreur pour déterminer quel serait le code d'état et le message corrects. Comme d'habitude avec les erreurs, vous devriez consulter la documentation sur les erreurs.

Sur la réponse, nous allons simplement consigner l'erreur dans 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)

Nous avons maintenant couvert la plupart des bases de la gestion des requêtes HTTP. À ce stade, vous devriez être en mesure de :

  • Instancier un serveur HTTP avec une fonction de gestionnaire de requêtes, et le faire écouter sur un port.
  • Obtenir les en-têtes, l'URL, la méthode et les données du corps à partir des objets request.
  • Prendre des décisions de routage en fonction de l'URL et/ou d'autres données dans les objets request.
  • Envoyer des en-têtes, des codes d'état HTTP et des données de corps via des objets response.
  • Transférer les données des objets request et vers les objets response.
  • Gérer les erreurs de flux dans les flux request et response.

À partir de ces bases, les serveurs HTTP Node.js pour de nombreux cas d'utilisation typiques peuvent être construits. Ces API offrent de nombreuses autres choses, alors assurez-vous de lire la documentation de l'API pour EventEmitters, Streams et HTTP.