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
.
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.
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.
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
.
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.
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.)
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 :
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
.
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
.
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
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.
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.
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
.
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.
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.
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 !
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
.
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
etresponse
.
À 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
.