Anatomie d'une requête HTTP
Un survol des détails des requêtes HTTP : leur histoire, leur place dans TCP/IP, et une analyse au microscope du mécanisme requête-réponse.Le protocole HTTP est au cœur des métiers de l’internet mais c’est quoi en fait une requête HTTP allez en route marcel (solo de guitare endiablé alors que la caméra s’envole du camion).
Un protocole est une procédure pré-établie que deux entités peuvent suivre pour arriver à un résultat : par exemple, dans le cas du protocole HTTP, à échanger de l’information.
En informatique, on peut ainsi implémenter un protocole dans n’importe quel langage en suivant sa spécification : c’est le document qui détaille toutes les règles du protocole.
Dans cet article, on va essayer de démystifier les requêtes HTTP en soulevant le capôt : Plutôt qu’utiliser un client GUI comme insomnia ou bruno, ou même un client CLI comme curl ou httpie, on va mettre les mains dans le cambouis et faire des requêtes HTTP manuellement.
Mais tout d’abord, un peu de théorie…
Spécification du protocole des requêtes HTTP
HTTP est donc un protocole défini par une spécification, sous forme de RFC. La version actuelle, la RFC 9110, “HTTP Semantics”, fait environ 200 pages. Elle détaille la sémantique de ce protocole : Les schemas URI, la gestion du cache, les headers, les méthodes, les status codes…
HTTP a été défini et redéfini au fil des années dans multiples RFCs (“HTTP/1.1” RFC 2616 en 1999, RFC 2068 en 1997, “HTTP/1.0”, RFC 1945 en 1996, …).
Ces documents sont longs et exhaustifs, mais le document qui a défini HTTP pour la première fois, en 1991, tient sur une seule page et défini ce qu’on appelle maintenant “HTTP/0.9”. Ce protocole est très simple et suit 4 étapes selon “le style du protocole telnet
”1 :
- La connexion : Le client se connecte via TCP/IP au serveur, via son domaine ou son IP et son port (par défaut 80).
- La requête : Une fois la connection établie, le client envoie une requête composée d’une seule ligne :
GET <chemin-du-document>\n
. - La réponse : Le serveur envoie un document HTML au format ASCII, puis ferme la connexion.
- La déconnexion ferme la connexion TCP/IP.
Au fil des années, le protocole à reçu des additions tant qu’à sa syntaxe et sémantique (verbes POST, DELETE etc, Headers, …) qu’aux façons d’établir une connexion (TCP vers UDP (QUIC), …). Cependant, une grosse partie est restée, ou est rétrocompatible.
À la RFC 9110 s’ajoutent la RFC 9111, 35 pages qui définissent les mécanismes de cache, et la RFC 9112, 46 pages qui définissent comment une requête HTTP/1.1 est encodée, décodée et comment la connexion s’opère, au niveau de la connexion TCP.
HTTP est défini en 3 RFCs pour pouvoir faire évoluer chacun de ces 3 axes indépendamment. En particulier, la RFC 9113 et la RFC 9114 définissent HTTP/2 et HTTP/3 respectivement. HTTP/3 utilise le protocole UDP plutôt que TCP, mais ces deux RFCs ne changent pas l’aspect sémantique du protocole.
La place des requêtes HTTP dans le framework TCP/IP
TCP/IP est une suite de protocoles qui, ensemble, composent ce qu’on appelle Internet. Ces protocoles sont organisés en “couches” empilées. L’idée architecturale est que chaque strate permet d’abstraire le niveau inférieur et en simplifier l’utilisation pour les strates supérieures.
HTTP se situe tout en haut de la pile, dans la couche “Application”, aux cotés de SSH, FTP, Gopher, Telnet, DNS…2
Vient ensuite la couche “Transport” : TCP et UDP. La couche d’en dessous est la couche “Réseau” : le protocole IP définit comment les appareils se connectent les uns aux autres. Enfin, les couches “Liaison” et “Physique” décrivent comment on échange à travers, par exemple, un câble Ethernet ou un réseau sans fil.
Une couche peut aussi permettre de répondre à certains problèmes de la couche précédente : par exemple, le protocole IP permet d’envoyer des packets de données, mais ne garantit pas que ces packets soient reçus par le destinataire, ou dans le bon ordre.
Le protocole TCP met en place des mécanismes pour palier à ces problèmes : entre autres, la connexion/déconnexion avec l’hôte distant et la réception de chaque paquet est confirmée à l’envoyeur, et celui-ci renvoie les packets “perdus”. Chaque packet est ordonné, et le receveur s’assure d’avoir tous les packets dans le bon ordre avant de les passer à la couche supérieure.
Ces mécanismes ajoutent des coûts (plus de packets et d’échanges). À l’inverse, UDP embrasse ces aspects au profit d’une plus grande vitesse de transfert par rapport à TCP. Ainsi, le développeur peut choisir le protocole adapté à ses besoins : fiabilité (HTTP, …) ou rapidité (voix/video en temps réel, …) : et ces deux protocoles utilisent le même protocole IP.
Étudier une requête HTTP/1.1 avec curl
HTTP/0.9 est depuis longtemps déprécié (1996 !). Maintenant qu’on connaît la base du protocole, on va directement étudier HTTP/1.1 : sans rentrer dans les détails, cette version rajoute la plupart des éléments auxquels nous sommes familier (les méthodes, les headers et les status codes).
curl
est un outil pour envoyer des requêtes HTTP. On va faire une requête avec --verbose
pour que curl nous affiche toute la requête, plutôt que juste la réponse. Pour nos tests, on va requêter http://httpbin.org/uuid
3.
curl -v "http://httpbin.org/uuid"
GET /uuid HTTP/1.1
Host: httpbin.org
User-Agent: curl/8.4.0
Accept: */*
HTTP/1.1 200 OK
Date: Tue, 18 Jun 2024 12:29:01 GMT
Content-Type: application/json
Content-Length: 53
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
{
"uuid": "3574b13a-dcfd-4e8e-b0a5-cffb219fb201"
}
On peut distinguer 3 sections :
- Ce qu’envoie curl : Comparé à HTTP/0.9, il y a toujours le
GET <path>
, mais on voit maintenant la version du protocole (HTTP/1.1
) et les Headers (Host
,User-Agent
,Accept
). La fin de la requête est marquée par une ligne vide. - Ce que reçoit curl : On y voit le header de réponse avec la version HTTP, le status code
200 OK
, ainsi que les headers de la réponse (Date
,Content-Type
,Content-Length
, …). La fin de l’en-tête de réponse est elle aussi marquée par une ligne vide. - Le body de la réponse : Le reste. Ici, c’est un objet JSON avec un UUID dedans. Pour une requête HTML, ce serait le contenu en HTML de la page.
Jouer au client : envoyer une requête HTTP
Un protocole n’est pas réservé qu’aux machines. Les humains aussi établissent et suivent des protocoles entre eux : protocole de sécurité, protocole post-détection…
Et on peut aussi, bien entendu, suivre un protocole où l’un est un humain et l’autre est une machine : puisque la communication est entièrement codifiée, il n y a qu’à suivre les règles ! 4
Pour notre analyse, descendre jusqu’au niveau des segments TCP ou packets IP serait un peu trop complexe (et hors-sujet !), mais curl ou bruno ne nous laisserait rien à faire et je n’aurais rien à écrire. Entre en scène netcat
: un outil pour faire ”à peu près tout ce qui touche à TCP, UDP ou aux sockets Unix.” Grace à cet utilitaire, on va pouvoir bidouiller au niveau d’abstraction qui nous intéresse : une connexion TCP.
Voici un enregistrement asciinema5 d’une requête faite à la main :
La commande nc httpbin.org 80
va ouvrir une connexion TCP avec le serveur httpbin.org (la résolution DNS se fait automatiquement), sur le port80
.On envoie une requête pour la page /uuid
et un headerHost
.La fin de la requête est marquée par un saut de ligne. Le serveur envoie ensuite sa réponse. - On peut ensuite fermer la connexion en stoppant
nc
, avecCtrl+C
. 6
On y retrouve exactement le même contenu que quand on a fait la requête via curl
- sauf que cette fois, on a nous-même écrit la requête !
Jouer au serveur : répondre à une requête HTTP
Nous avons vu comment envoyer une requête. Dans l’autre sens, on va maintenant jouer le rôle du serveur. On utilisera à nouveau netcat, cette fois, pour accepter des connexions TCP.
Cet enregistrement asciinema est accompagné d’une représentation du navigateur, pour aider à visualiser l’interaction client-serveur :
La commande nc -l 8080
va ouvrir le port 8080 (-l pour listen).On pointe notre navigateur internet sur http://localhost:8080/ma-page
…nc
receptionne la requête du navigateur. On retrouve le verbeGET
, le chemin voulu/ma-page
, et la version du protocole,HTTP/1.1
. Ensuite sont listés des headers, puis un\n\n
pour signifier la fin de la requête.On peut maintenant répondre à la requête du navigateur à la main, en suivant le protocole HTTP. Ici, on indique au navigateur que tout s’est bien passé (200 OK), qu’on envoie un document HTML (header Content-Type). On saute une ligne pour signifier la fin des Headers, puis on envoie le contenu : une page HTML. Enfin7, on ferme la connexion avec Ctrl+C
. Le navigateur peut maintenant afficher notre page HTML !
Cette page est évidemment très simple, mais ce mécanisme est la base de toute requête HTTP. Ce que nous venons de faire, c’est en gros ce que chaque serveur Web fait ! (Bien entendu, de manière plus rapide et surtout, beaucoup plus complexe.)
Conclusion
À travers ces exemples, nous avons vu à quoi ressemble une requête HTTP, et comment un client (comme un navigateur web) et un serveur échangent des données.
Un client web va donc ouvrir une connexion vers un serveur distant et envoyer une requête, soit pour récupérer de la donnée, soit pour en envoyer.
Un serveur web, quant à lui, est un programme qui va écouter sur un port, accepter des requêtes, les traiter selon le protocole HTTP, et suivre sa configuration pour envoyer une réponse. Celle-ci peut être codée en dur (comme dans notre cas, en mémoire), ou le serveur devrait servir le contenu d’un fichier statique, ou passer la requête à un autre serveur (reverse-proxy, load-balancing, …) ou bien encore exécuter du code et générer une réponse de toute part.
On peut intuitivement en déduire que moins le serveur web à d’actions à effectuer pour répondre à une requête, plus il sera rapide à répondre. C’est ce qu’illustre entre autres l’article ”35 Million Hot Dogs: Benchmarking Caddy vs. Nginx” (englais) : peu importe le serveur, répondre avec un fichier statique est en général au moins deux fois plus rapide que de devoir reverse-proxy la réponse à un autre serveur (et c’est sans compter le temps de traitement de cet autre serveur !). 8
En pratique, c’est un des grands avantages des Sites Statiques : tous les fichiers du site sont pré-générés à l’avance via un SSG, et le serveur web n’a plus qu’à servir ces fichiers. À l’opposé, un Site Dynamique devra, à chaque requête, exécuter souvent plusieurs centaines de lignes de code afin de construire la page demandée.
Footnotes
-
“Le style du protocole
telnet
” veut dire : l’utilisateur se connecte à une machine distante, via TCP, de manière interactive (requête-réponse), et text-based. Beaucoup des protocoles de cette époque suivent ce format et sont conçus pour être lu par un humain (IMAP/SMTP, …). ↩ -
Httpbin est un service en ligne pour tester les requêtes HTTP. L’endpoint
/uuid
répond simplement avec un UUID. ↩ -
En général, les applications sont programmées de manière robuste pour ne pas crash quand le protocole n’est plus suivi (par exemple si un client parle un autre protocole) : La connexion est fermée, et un message d’erreur est loggé sur le serveur ou affiché par le navigateur. ↩
-
Asciinema est un outil qui enregistre le contenu du terminal. Si ça vous tente, vous pouvez reproduire l’expérience dans votre propre terminal ! ↩
-
On peut remarquer qu’ici, la connexion reste ouverte. C’est une des améliorations par rapport à HTTP/0.9 : le serveur ne ferme plus la connexion après la réponse, et on peut lui envoyer de nouvelles requêtes ! ↩
-
Il n’est cependant pas nécessaire de fermer la connexion ! Elle pourrait être réutilisée pour de futurs échanges (requêtes, SSE, header 103 Early Hints…) ↩
-
La vitesse de réponse dépend bien entendu de la taille de la réponse, et donc du fichier envoyé. Dans ces benchmarks, un fichier de 109K met environ 3 à 5 fois plus de temps qu’une réponse de 4.5K ! On voit ainsi tout l’intérêt de minimiser ses assets, minifier son code, et bien gérer son cache. ↩