De HTTP/1.0 à HTTP/3 : comment le web a appris à ne plus attendre

Chaque version de HTTP est née pour supprimer une attente : les connexions rouvertes sans fin, les réponses servies à la file, le paquet perdu qui bloque toute la page. On suit une même page de HTTP/1.0 à HTTP/3, avec des diagrammes de séquence, des schémas et des commandes curl pour tout vérifier soi-même.

Catégorie Fondamentaux & langages

Temps de lecture 27 min

Ouvre n'importe quel site. Avant que la page s'affiche, ton navigateur a demandé un fichier HTML, puis une feuille de style, des scripts, des images : parfois une centaine de ressources. Chacune passe par **HTTP**, le protocole qui fait parler ton navigateur et le serveur. Depuis 1996, HTTP a connu quatre versions majeures : HTTP/1.0, HTTP/1.1, HTTP/2 et HTTP/3. Le sens des messages n'a presque pas bougé : on demande toujours une ressource avec une méthode, un chemin et des en-têtes, et on reçoit toujours un code de statut et un contenu. Ce qui a changé, c'est **la façon de transporter ces messages**. Et chaque nouvelle version est née d'un seul ennemi : **l'attente**. Cet article suit une même page, `index.html` avec sa feuille de style et son logo, à travers les quatre versions. Pour chacune, tu verras ce qu'elle apporte, où elle fait attendre, et comment la suivante corrige le problème. Chaque mécanisme a son diagramme, et chaque affirmation se vérifie avec `curl` sur ta propre machine. Pour lire les diagrammes de séquence : le temps s'écoule de haut en bas. Une flèche pleine est une requête, une flèche en pointillés une réponse, une flèche dorée ce qui compte dans l'étape, et une croix dorée un paquet perdu en route. Avant les versions : ce que fait HTTP Imagine un restaurant. Tu passes une commande, « un plat de ndolé », le serveur va en cuisine et revient avec le plat, ou avec une réponse : « il n'y en a plus ». Pour que ça marche, vous parlez la même langue et suivez les mêmes règles. HTTP, c'est cette langue entre ton navigateur (le **client**) et la machine qui héberge le site (le **serveur**). Le client envoie une **requête**, le serveur renvoie une **réponse**. Rien d'autre. Quatre notions suffisent pour suivre la suite : **Adresse IP et port** : l'adresse IP identifie une machine sur le réseau, comme l'adresse d'un immeuble ; le port identifie un programme sur cette machine, comme un numéro d'appartement. HTTP écoute par défaut sur le port 80, HTTPS sur le port 443. **DNS** : l'annuaire d'Internet. Il traduit un nom lisible, `example.com`, en adresse IP. **TCP** : le protocole de transport qui garantit que les octets arrivent complets et dans l'ordre. Il ouvre une connexion entre les deux machines avant d'envoyer quoi que ce soit. **RFC** : un document officiel de l'IETF, l'organisme qui normalise les protocoles d'Internet. Chaque version de HTTP est décrite par une RFC numérotée. Quand on veut la règle exacte, on lit la RFC. HTTP ne transporte rien lui-même. Il s'appuie sur un protocole de transport : TCP jusqu'à HTTP/2, QUIC pour HTTP/3. Toute l'histoire qui suit tient dans cette relation entre HTTP et ce qu'il y a dessous. Ce qui se passe quand tu tapes une URL Avant le moindre octet HTTP, le navigateur doit trouver le serveur, puis ouvrir un canal vers lui. Trois protocoles se relaient : Diagramme de séquence : le navigateur interroge le DNS, ouvre une connexion TCP en trois messages, puis envoie la requête HTTP DNS donne l'adresse, TCP ouvre le canal, HTTP donne le sens de l'échange. Le **three-way handshake** de TCP, la « poignée de main » en trois messages, sert à ouvrir la connexion : SYN (« je veux te parler »), SYN-ACK (« d'accord, moi aussi »), ACK (« bien reçu »). Garde-le en tête : c'est lui qui coûtera cher dans les premières versions. Une requête, c'est du texte En HTTP/1.x, une requête est du texte structuré en quatre parties : GET /index.html HTTP/1.1 ← ligne de requête : méthode, chemin, version Host: example.com ← en-têtes, au format Nom: valeur User-Agent: curl/8.5.0 Accept: text/html ← ligne vide : fin des en-têtes ← corps, vide pour un GET La **méthode** dit ce que tu veux faire de la ressource : `GET` lit une ressource, sans corps dans la requête ; `POST` envoie des données (un formulaire, un nouvel objet) ; `PUT` remplace une ressource, `PATCH` la modifie en partie ; `DELETE` la supprime ; `HEAD` fait comme `GET`, mais sans le corps de la réponse. Les **en-têtes** ajoutent des informations : qui demande, quels formats sont acceptés, la taille du contenu. C'est la partie qui change le plus d'une version à l'autre. Une réponse aussi HTTP/1.1 200 OK ← ligne de statut : version, code, message Content-Type: text/html ← en-têtes Content-Length: 1256 ← ligne vide <!doctype html>… ← corps Le premier chiffre du **code de statut** donne sa famille : `1xx`, information : `101 Switching Protocols` ; `2xx`, succès : `200 OK`, `201 Created`, `204 No Content` ; `3xx`, redirection : `301 Moved Permanently`, `304 Not Modified` ; `4xx`, erreur du client : `400`, `401`, `403`, `404` ; `5xx`, erreur du serveur : `500`, `502`, `503`. Le voir avec curl L'option `-v` de `curl` affiche tout ce qui est envoyé (lignes `>`) et reçu (lignes `<`) : curl -v http://example.com/ * Trying <adresse IP>:80... ← curl a obtenu l'IP auprès du DNS * Connected to example.com port 80 ← connexion TCP ouverte > GET / HTTP/1.1 ← ligne de requête > Host: example.com ← en-têtes envoyés > User-Agent: curl/8.5.0 > Accept: */* > ← ligne vide : fin de la requête < HTTP/1.1 200 OK ← ligne de statut reçue < Content-Type: text/html ← en-têtes reçus < Content-Length: 1256 < <!doctype html>… ← corps Les valeurs exactes changent selon ta machine et le site. La structure, elle, ne change jamais. HTTP ne se souvient de rien HTTP est **sans état** : le serveur ne garde aucun souvenir des requêtes précédentes. Chaque requête doit porter tout ce dont il a besoin. Pour « reconnaître » un utilisateur, on ajoute une information dans les en-têtes, en général un **cookie** : Diagramme de séquence : le serveur pose un cookie de session à la connexion, puis le navigateur le renvoie à chaque requête Le cookie voyage dans chaque requête : c'est lui qui donne une mémoire à un protocole qui n'en a pas. Le vrai sujet : le RTT Le **RTT** (Round-Trip Time) est le temps d'un aller-retour sur le réseau, du client au serveur et retour. Sur une bonne fibre, quelques dizaines de millisecondes ; sur un réseau mobile, souvent plus de 100 ms. Et ce temps ne dépend pas de la puissance des machines : c'est la distance et la qualité du réseau qui le fixent. On ne peut pas raccourcir un aller-retour. On peut seulement en faire moins. Toute l'histoire de HTTP tient en une question : combien d'allers-retours faut-il perdre avant d'obtenir ce qu'on a demandé ? Chaque version en supprime quelques-uns. HTTP/1.0 : une commande, une visite (1996) Retour au restaurant. En HTTP/1.0, chaque fois que tu veux quelque chose, tu entres, tu commandes, tu reçois ton plat… et tu ressors. Pour la boisson, tu rentres à nouveau, tu recommandes, tu ressors. Pour le dessert, pareil. Simple, mais tu passes ton temps à franchir la porte. **HTTP/1.0** (RFC 1945, 1996) fixe le format texte requête et réponse, avec ses en-têtes et ses codes de statut. Sa règle de transport tient en une phrase : **une connexion TCP par requête**, fermée juste après la réponse. Ce que HTTP/1.0 apporte Avant lui, HTTP/0.9 ne connaissait qu'une ligne, `GET /page`, et ne renvoyait que du HTML brut. HTTP/1.0 ajoute presque tout ce qu'on utilise encore aujourd'hui : HTTP/0.9 (requête) HTTP/1.0 (requête) GET /index.html GET /index.html HTTP/1.0 ← + la version User-Agent: Mozilla/3.0 ← + les en-têtes Accept: text/html (ligne vide) ← + la fin des en-têtes

HTTP/0.9 (réponse) HTTP/1.0 (réponse) <html>…</html> HTTP/1.0 200 OK ← + la ligne de statut Content-Type: text/html ← + le type du contenu Content-Length: 1256 ← + la taille (ligne vide) <html>…</html> **La version dans la requête** : le serveur sait quelle « langue » parle le client. **Les méthodes `POST` et `HEAD`**, en plus de `GET` : on peut enfin envoyer des données, par exemple un formulaire. **Les en-têtes**, pour décrire la requête et la réponse. **Les codes de statut** (200, 404…) : on sait si ça a marché. **`Content-Type`** : on peut envoyer autre chose que du HTML, des images, du CSS, des PDF. Un absent de taille : l'en-tête `Host`, qui indique le nom du site demandé, n'est pas obligatoire. Un serveur qui héberge plusieurs sites sur la même adresse IP ne sait donc pas facilement lequel on lui demande. Une ressource, une connexion Une vraie page demande plusieurs ressources. Avec HTTP/1.0, chacune repaie l'ouverture et la fermeture d'une connexion : Diagramme de séquence : trois connexions TCP successives, chacune avec son handshake, sa requête, sa réponse et sa fermeture Trois ressources, trois connexions : le handshake est rejoué à chaque fois. Le prix : le handshake répété Regarde ce que coûte une seule ressource. Le handshake prend un aller-retour entier avant que la requête puisse partir : Diagramme de séquence : un RTT pour le handshake TCP, un second RTT pour la requête et sa réponse Deux allers-retours par ressource, dont un seul transporte quelque chose d'utile. Fais le calcul pour notre page de trois ressources, sans HTTPS : 3 × (1 RTT de handshake + 1 RTT de requête) = **6 RTT**. Avec un RTT de 100 ms, fréquent sur un réseau mobile, cela fait 600 ms passées uniquement à attendre. Avec HTTPS, la négociation du chiffrement ajoute encore un ou deux allers-retours… par connexion. Certains navigateurs et serveurs ont ajouté une extension non standard, `Connection: keep-alive`, pour garder la connexion ouverte. Elle n'était pas garantie : c'est HTTP/1.1 qui en fera le comportement par défaut. L'option `--http1.0` force `curl` à parler cette version : curl -v --http1.0 http://example.com/ > GET / HTTP/1.0 ← la version annoncée est 1.0 > Host: example.com ← curl l'ajoute quand même, par politesse > User-Agent: curl/8.5.0 < HTTP/1.0 200 OK < Content-Type: text/html * Closing connection ← la connexion est fermée après la réponse HTTP/1.0 : une requête, une connexion TCP. Sa limite : le handshake rejoué pour chaque ressource. HTTP/1.1 : on reste à table (1997) Au restaurant, tu ne ressors plus après chaque plat : tu restes assis à ta table. Tu commandes le plat, puis la boisson, puis le dessert, sans franchir la porte à chaque fois. Seul défaut : le serveur ne t'apporte toujours qu'une chose à la fois, dans l'ordre où tu as commandé. **HTTP/1.1** (RFC 2068 en 1997, révisée en RFC 2616 puis aujourd'hui en RFC 9112) rend la **connexion persistante par défaut** : une seule connexion TCP sert plusieurs requêtes. Il rend aussi l'en-tête **`Host` obligatoire**, ce qui permet d'héberger plusieurs sites sur une même adresse IP (on parle d'**hébergement virtuel**). Une requête HTTP/1.1 sans `Host` reçoit un `400 Bad Request`. Ce qui change dans la requête HTTP/1.0 HTTP/1.1 GET /index.html HTTP/1.0 GET /index.html HTTP/1.1 ← version 1.1 User-Agent: Mozilla/3.0 Host: example.com ← + obligatoire Accept: text/html User-Agent: Mozilla/5.0 Accept: text/html Connection: keep-alive ← + le comportement par défaut If-None-Match: "abc123" ← + le cache conditionnel HTTP/1.1 apporte aussi une série d'outils qu'on utilise encore partout : **`Connection: close`** pour annoncer la dernière requête et fermer la connexion ; **`Transfer-Encoding: chunked`** pour envoyer une réponse par morceaux sans connaître sa taille à l'avance ; **`ETag` et `If-None-Match`** : si la ressource n'a pas changé, le serveur répond `304 Not Modified` sans la renvoyer ; **`Cache-Control`** : des règles de cache plus précises que l'ancien `Expires` ; **`Range`** et la réponse `206 Partial Content` : reprendre un téléchargement, lire une vidéo à partir d'un point précis ; **les méthodes `PUT`, `DELETE` et `OPTIONS`**, la base des API REST. Une réponse découpée en morceaux ressemble à ceci : chaque morceau commence par sa taille en hexadécimal, et un morceau de taille 0 marque la fin. HTTP/1.1 200 OK Content-Type: text/html Transfer-Encoding: chunked

1a <html><body>Bonjour</body> 0 Une connexion, trois requêtes La même page, cette fois sur une seule connexion : Diagramme de séquence : un seul handshake TCP, trois couples requête et réponse, une seule fermeture Un seul handshake pour toute la page. Connection: close annonce la dernière requête. Le gain se compte facilement : 1 RTT de handshake + 3 RTT de requêtes = **4 RTT**, contre 6 en HTTP/1.0. Avec HTTPS, l'écart grandit encore, puisque la négociation TLS n'est faite qu'une fois. `curl` le montre si tu lui donnes deux URL dans la même commande : curl -v --http1.1 http://example.com/ http://example.com/ * Connected to example.com port 80 ← une seule ouverture TCP > GET / HTTP/1.1 < HTTP/1.1 200 OK * Connection #0 to host example.com left intact ← la connexion reste ouverte * Re-using existing connection with host example.com ← 2e requête, même connexion > GET / HTTP/1.1 Le nouveau problème : la file d'attente Sur une connexion HTTP/1.1, le client attend la réponse 1 avant d'envoyer la requête 2. Le standard a bien prévu le **pipelining** : envoyer plusieurs requêtes sans attendre. Mais le serveur doit répondre **dans l'ordre**. Si la première réponse est lente, toutes les autres attendent derrière elle : Diagramme de séquence : trois requêtes envoyées d'affilée, la première réponse est lente et retarde les deux suivantes déjà prêtes Une réponse lente bloque toutes celles qui la suivent : c'est le blocage en tête de file. C'est le **blocage en tête de file** (*Head-of-Line blocking*) : un élément lent bloque tous ceux qui sont derrière lui, comme une voiture en panne à un péage à une seule voie. Le pipelining est désactivé par défaut dans tous les navigateurs : trop de serveurs et de proxys (les machines intermédiaires entre le client et le serveur) le géraient mal. En pratique, HTTP/1.1, c'est une requête à la fois par connexion. La parade des navigateurs : six connexions Pour paralléliser malgré tout, les navigateurs ouvrent jusqu'à environ **six connexions TCP simultanées par domaine** : Diagramme de séquence : trois connexions ouvertes en parallèle, chacune avec son handshake et sa requête Le parallélisme revient, mais le prix du handshake aussi. On retrouve en partie le coût de HTTP/1.0. Les développeurs ont empilé d'autres astuces pour faire moins de requêtes : concaténer tous les fichiers JS et CSS en un seul, regrouper les icônes dans une seule image (les *sprites*), répartir les ressources sur plusieurs domaines pour obtenir plus de connexions (le *domain sharding*). HTTP/2 rendra toutes ces astuces inutiles. HTTP/1.1 : une connexion sert plusieurs requêtes, mais une à la fois et dans l'ordre. Sa limite : le blocage en tête de file au niveau HTTP. HTTP/2 : des tickets numérotés (2015) Toujours à ta table. Cette fois, tu passes toutes tes commandes d'un coup, et chaque élément porte un numéro de ticket. Le serveur t'apporte ce qui est prêt en premier, même si c'est le dessert, et tu reconnais chaque chose grâce à son numéro. Un plat long à préparer ne bloque plus le reste. **HTTP/2** (RFC 7540 en 2015, aujourd'hui RFC 9113) garde le sens de HTTP (méthodes, en-têtes, codes de statut) mais change complètement sa forme sur le réseau : les messages sont découpés en **frames binaires**, et plusieurs requêtes circulent en même temps sur **une seule connexion TCP**. Du texte au binaire : les frames En HTTP/1.1, le serveur lit du texte caractère par caractère jusqu'à trouver une ligne vide. En HTTP/2, tout est découpé en **frames**, de petits blocs dont chaque octet a une place précise : Schéma d'une frame HTTP/2 : Length sur 24 bits, Type sur 8 bits, Flags sur 8 bits, Stream ID sur 31 bits, puis le contenu Un en-tête fixe de 9 octets : le receveur sait toujours combien lire, et pour quel échange. Chaque frame appartient à un **stream** : un échange requête-réponse, repéré par son numéro (*Stream ID*). Les streams ouverts par le client portent des numéros impairs : 1, 3, 5… Une requête, c'est une frame `HEADERS` (plus des frames `DATA` s'il y a un corps) ; une réponse, c'est `HEADERS` puis `DATA`. Les principaux types de frames : **`HEADERS`** : les en-têtes d'une requête ou d'une réponse ; **`DATA`** : le corps (HTML, JSON, image…) ; **`SETTINGS`** : les paramètres de la connexion, échangés au démarrage ; **`WINDOW_UPDATE`** : le contrôle de flux, « tu peux m'envoyer N octets de plus » ; **`RST_STREAM`** : annuler un seul stream sans fermer la connexion ; **`PING`** et **`GOAWAY`** : vérifier la connexion, annoncer sa fermeture. La même requête, en HTTP/1.1 et en HTTP/2 Le sens est identique, la forme change. Les informations de la première ligne deviennent des **pseudo-en-têtes**, qui commencent par `:` : HTTP/1.1 (texte) HTTP/2 (frame HEADERS, stream 1, décodée) GET /index.html HTTP/1.1 :method: GET ← la méthode :path: /index.html ← le chemin :scheme: https ← + le schéma Host: example.com :authority: example.com ← remplace Host User-Agent: Mozilla/5.0 user-agent: Mozilla/5.0 ← noms en minuscules Accept: text/html accept: text/html Connection: keep-alive (interdit) ← supprimé

HTTP/1.1 200 OK :status: 200 ← plus de « OK » Content-Type: text/html content-type: text/html Transfer-Encoding: chunked (interdit) ← les frames DATA découpent déjà [frame DATA, END_STREAM] ← le corps, dans sa propre frame Les en-têtes `Connection`, `Keep-Alive`, `Transfer-Encoding` et `Upgrade` sont interdits : la connexion est désormais gérée par HTTP/2 lui-même. Le démarrage : TLS, ALPN et SETTINGS En pratique, les navigateurs ne parlent HTTP/2 qu'à travers **TLS**, le chiffrement de HTTPS. Le choix du protocole se fait pendant la négociation TLS grâce à l'extension **ALPN** : le client annonce ce qu'il sait parler (`h2`, `http/1.1`), le serveur choisit. Aucun aller-retour supplémentaire. Diagramme de séquence : handshake TCP, négociation TLS avec ALPN qui choisit h2, puis échange des frames SETTINGS Le protocole se choisit dans le handshake TLS ; les deux côtés échangent ensuite leurs réglages. Le multiplexing Voici le cœur de HTTP/2. Les frames de plusieurs streams s'entrelacent sur la même connexion, et le receveur les regroupe grâce à leur numéro. C'est le **multiplexing** : Diagramme de séquence : trois requêtes sur les streams 1, 3 et 5 ; les réponses du CSS et de l'image arrivent avant celle du HTML, en morceaux entrelacés index.html est lent, mais le CSS et l'image ne l'attendent plus : chaque frame dit à quel stream elle appartient. Compare avec le pipelining de HTTP/1.1 : les réponses arrivent dans n'importe quel ordre, et une ressource lente ne bloque plus les autres. **Le blocage en tête de file au niveau HTTP disparaît.** Plus besoin de six connexions, de fichiers concaténés ni de sprites. HPACK : ne plus répéter les en-têtes En HTTP/1.1, des en-têtes comme `User-Agent` ou `Cookie` sont renvoyés en entier à chaque requête, parfois plusieurs centaines d'octets identiques. **HPACK** (RFC 7541) garde des deux côtés une table des en-têtes déjà vus. À la deuxième requête, un en-tête connu n'est plus qu'un numéro dans cette table : Stream 1 : user-agent: Mozilla/5.0 (… 120 octets …) ← envoyé en entier, ajouté à la table cookie: session=abc123 (… 80 octets …) Stream 3 : [index 62] [index 63] ← quelques octets HTTP/2 avait aussi prévu le *server push* : le serveur envoyait une ressource avant qu'on la demande. Mal utilisé et difficile à régler, il a été retiré des navigateurs. On préfère aujourd'hui des indices comme `<link rel="preload">`. Avec `curl`, tu vois la négociation et les pseudo-en-têtes : curl -v --http2 -o /dev/null https://www.google.com/ * ALPN: curl offers h2,http/1.1 * ALPN: server accepted h2 ← HTTP/2 négocié dans TLS * [HTTP/2] [1] OPENED stream for https://www.google.com/ * [HTTP/2] [1] [:method: GET] ← stream 1, pseudo-en-têtes * [HTTP/2] [1] [:scheme: https] * [HTTP/2] [1] [:authority: www.google.com] * [HTTP/2] [1] [:path: /] > GET / HTTP/2 ← traduction lisible, pas ce qui circule < HTTP/2 200 ← :status 200, sans « OK » < content-type: text/html; charset=ISO-8859-1 ← noms en minuscules La limite qui reste : TCP HTTP/2 multiplexe, mais TCP ne le sait pas. Pour TCP, il n'y a qu'un seul flux d'octets, qu'il doit livrer dans l'ordre. Si un seul paquet se perd, TCP retient tout ce qui arrive après lui jusqu'à sa retransmission, y compris les frames d'autres streams déjà arrivées : Diagramme de séquence : le paquet TCP 2 du stream 3 est perdu ; le paquet 3 du stream 5 arrive mais reste bloqué jusqu'à la retransmission Un paquet perdu, et ce sont tous les streams qui attendent. Le blocage en tête de file n'a pas disparu : il est descendu d'un étage, au niveau du transport. Sur un réseau qui perd des paquets (mobile, Wi-Fi chargé), HTTP/2 peut même faire moins bien que six connexions HTTP/1.1, car une seule perte freine toute la page. HTTP/2 : des frames binaires et des streams multiplexés sur une seule connexion TCP, avec des en-têtes compressés. Sa limite : un paquet TCP perdu bloque tous les streams. HTTP/3 : un couloir par commande (2022) En HTTP/2, tes commandes numérotées passent toutes par un seul couloir. Si un serveur trébuche avec ta boisson, tout le monde derrière lui attend qu'il se relève, même ceux qui portent ton plat et ton dessert. HTTP/3 donne **un couloir à chaque commande**. Si la boisson tombe, seule la boisson est refaite ; le plat et le dessert arrivent normalement. Pour construire ces couloirs séparés, il a fallu abandonner TCP. **HTTP/3** (RFC 9114, 2022) garde les idées de HTTP/2 (streams, frames, en-têtes compressés) mais remplace TCP par **QUIC** (RFC 9000), un nouveau transport construit au-dessus d'**UDP**, qui intègre le chiffrement TLS 1.3 et gère lui-même les streams. Changer de transport : QUIC sur UDP Schéma des piles : HTTP/1.1 et HTTP/2 reposent sur TLS et TCP ; HTTP/3 repose sur QUIC, qui intègre TLS 1.3, au-dessus d'UDP HTTP/3 ne change pas HTTP : il remplace ce qu'il y a dessous. **UDP** est l'autre grand protocole de transport. Il envoie des paquets sans connexion, sans garantir ni leur arrivée ni leur ordre. C'est justement ce qui le rend utile ici : il ne fait presque rien, donc on peut tout construire au-dessus. Pourquoi ne pas simplement améliorer TCP ? Parce que TCP est implémenté dans le **noyau** des systèmes d'exploitation et dans des millions de routeurs et de pare-feu : le modifier prend des années, voire des décennies. QUIC, lui, tourne dans l'application (le navigateur, le serveur web) et se met à jour comme n'importe quel logiciel. Il réimplémente ce que TCP faisait (fiabilité, ordre, contrôle de congestion) et y ajoute des streams indépendants et le chiffrement intégré. Ce qui change dans les frames Côté requête, presque rien : HTTP/3 garde les mêmes pseudo-en-têtes et les mêmes noms en minuscules. Ce qui change, c'est l'enveloppe : HTTP/2 (frame HEADERS) HTTP/3 (frame HEADERS) [Length | Type | Flags | Stream ID] [Type | Length] ← plus de Stream ID ni de Flags :method: GET :method: GET ← mêmes pseudo-en-têtes :scheme: https :scheme: https :authority: example.com :authority: example.com :path: /index.html :path: /index.html (compressé avec HPACK) (compressé avec QPACK) ← nouvelle compression **Le numéro de stream** n'est plus dans la frame : c'est QUIC qui le porte. **La fin d'un stream** n'est plus un drapeau `END_STREAM` : le stream QUIC se ferme. **Le contrôle de flux, le ping et l'annulation** sont gérés par QUIC ; les frames `WINDOW_UPDATE`, `PING` et `RST_STREAM` disparaissent de HTTP/3. **La compression des en-têtes** passe de HPACK à **QPACK** (RFC 9204). **Les streams du client** sont numérotés 0, 4, 8… au lieu de 1, 3, 5. Un handshake plus court Avec TCP et TLS 1.3, il faut d'abord ouvrir la connexion TCP, puis négocier le chiffrement. QUIC fait les deux en même temps : Diagramme de séquence comparé : TCP puis TLS 1.3 prennent deux allers-retours avant la requête, QUIC un seul Transport et chiffrement négociés ensemble : un aller-retour de gagné à chaque nouvelle connexion. Mieux : si le client a déjà parlé à ce serveur, il garde un ticket de session et peut envoyer sa requête **dès le premier paquet**. C'est le **0-RTT** : Diagramme de séquence : le client envoie le ClientHello et sa requête GET dans le premier paquet QUIC Le 0-RTT : la requête part avant même la fin du handshake. Les données envoyées en 0-RTT peuvent être rejouées par un attaquant qui les intercepte. On les réserve donc aux requêtes sans effet de bord, comme un `GET`, jamais à un paiement ou à une commande. Des streams vraiment indépendants QUIC connaît les streams. Un paquet perdu ne bloque que le stream auquel il appartient : Diagramme de séquence : le paquet du stream 4 est perdu ; les streams 0 et 8 sont livrés tout de suite, seul le stream 4 attend sa retransmission Même perte qu'avec HTTP/2, mais cette fois seul le CSS attend. Compare avec le diagramme de HTTP/2 : **le blocage en tête de file au niveau du transport disparaît**. C'est aussi pour cela que HTTP/3 a dû remplacer HPACK par QPACK : HPACK suppose que les en-têtes arrivent dans l'ordre, ce que QUIC ne garantit plus d'un stream à l'autre. Changer de réseau sans couper Une connexion TCP est identifiée par quatre valeurs : IP et port du client, IP et port du serveur. Quand ton téléphone passe du Wi-Fi à la 4G, son IP change et la connexion meurt : il faut tout rouvrir. QUIC identifie la connexion par un **Connection ID**, indépendant de l'adresse : Diagramme de séquence : le téléphone passe du Wi-Fi à la 4G en gardant le même Connection ID ; le serveur vérifie le nouveau chemin et la connexion continue La migration de connexion : même conversation, nouveau réseau, aucun nouveau handshake. Comment le navigateur découvre HTTP/3 Le navigateur ne sait pas d'avance si un serveur parle HTTP/3. En général, la première visite se fait en HTTP/2, et le serveur annonce HTTP/3 avec l'en-tête **`Alt-Svc`** : Diagramme de séquence : la première réponse en HTTP/2 contient Alt-Svc h3, les requêtes suivantes passent par QUIC Le serveur annonce HTTP/3 ; le navigateur s'en souvient pour la suite. Un enregistrement DNS de type `HTTPS` (RFC 9460) peut aussi l'annoncer avant même la première connexion. Les limites de HTTP/3 Certains réseaux d'entreprise bloquent ou brident UDP. Le navigateur revient alors à HTTP/2, sans que l'utilisateur s'en aperçoive. QUIC tourne dans l'application et chiffre presque tout, y compris ses propres en-têtes : il coûte plus de CPU côté serveur et se laisse moins facilement analyser par les outils réseau. Pour l'essayer avec `curl`, vérifie d'abord que ta version le supporte : `curl -V` doit afficher `HTTP3`. curl -v --http3 -o /dev/null https://www.cloudflare.com/ * using HTTP/3 * [HTTP/3] [0] OPENED stream for https://www.cloudflare.com/ ← stream 0, et non 1 * [HTTP/3] [0] [:method: GET] * [HTTP/3] [0] [:scheme: https] * [HTTP/3] [0] [:authority: www.cloudflare.com] * [HTTP/3] [0] [:path: /] > GET / HTTP/3 < HTTP/3 200 < content-type: text/html; charset=utf-8 HTTP/3 : HTTP sur QUIC, c'est-à-dire UDP avec TLS 1.3 intégré. Ses gains : un handshake en un aller-retour (zéro en reprise), des pertes isolées stream par stream, une connexion qui survit à un changement de réseau. Le match des quatre versions Chaque version corrige la limite principale de la précédente. HTTP/1.1 évite les handshakes répétés, HTTP/2 supprime le blocage au niveau HTTP, HTTP/3 supprime le blocage au niveau du transport : Tableau comparatif de HTTP/1.0, HTTP/1.1, HTTP/2 et HTTP/3 : année, format, transport, connexions, multiplexing, compression, chiffrement, blocages et limite principale Le sens de HTTP ne change pas ; son transport, si. La même requête, quatre écritures On demande `/index.html` sur `example.com`. Les lignes marquées `+` apparaissent avec la version, celles marquées `-` disparaissent : HTTP/1.0 HTTP/1.1 GET /index.html HTTP/1.0 GET /index.html HTTP/1.1 User-Agent: … + Host: example.com Accept: text/html User-Agent: … Accept: text/html + Connection: keep-alive texte, sur TCP texte, sur TCP

HTTP/2 (HEADERS, stream 1) HTTP/3 (HEADERS, stream 0) :method: GET :method: GET :path: /index.html :path: /index.html :scheme: https :scheme: https :authority: example.com :authority: example.com user-agent: … user-agent: … accept: text/html accept: text/html - (Connection interdit) (interdit) binaire, HPACK, TCP + TLS binaire, QPACK, QUIC (UDP + TLS 1.3) Le sens ne bouge jamais : une méthode, un chemin, un site, des en-têtes. Seules changent la façon de l'écrire et celle de le transporter. La même page, quatre trajets Notre page de trois ressources, sur HTTPS, avec un `index.html` lent à générer. Le diagramme montre où chaque version attend : Diagramme de séquence en quatre blocs : HTTP/1.0 ouvre trois connexions, HTTP/1.1 enchaîne les réponses sur une connexion, HTTP/2 multiplexe trois streams, HTTP/3 fait de même sur QUIC De trois connexions et six allers-retours à une connexion QUIC où chaque réponse part dès qu'elle est prête. Les allers-retours avant la première réponse Sur une nouvelle connexion HTTPS, voici ce qu'il faut perdre avant de pouvoir seulement envoyer la requête : Graphique : TLS 1.2 coûte 3 RTT avant la requête, TLS 1.3 sur TCP 2 RTT, QUIC 1 RTT, QUIC en reprise 0 RTT Sur un réseau mobile à 100 ms d'aller-retour, la différence entre la première et la dernière ligne est de 300 ms, à chaque nouvelle connexion. Rappel : en HTTP/1.0, ce coût est payé pour chaque ressource ; à partir de HTTP/1.1, une seule fois par connexion. Quelle version dans quel cas **HTTP/1.1** reste universel : outils en ligne de commande, API simples, équipements anciens. C'est aussi le meilleur format pour comprendre HTTP, puisqu'il se lit à l'œil nu. **HTTP/2** est le bon choix par défaut pour un site ou une API derrière un serveur moderne (Nginx, Caddy, un CDN). **HTTP/3** apporte le plus sur les réseaux mobiles ou instables, avec des pertes de paquets et des changements de réseau : exactement les conditions de beaucoup d'utilisateurs en 3G ou en 4G. Tu n'as pas à choisir pour tes visiteurs : un serveur sert en général les trois versions à la fois, et chaque client prend la meilleure qu'il sait parler. Ce qu'il faut retenir HTTP est un protocole de requêtes et de réponses posé sur un transport. Son sens n'a presque pas changé en trente ans ; son transport, si. **HTTP/1.0** ouvre une connexion par ressource et rejoue le handshake à chaque fois. **HTTP/1.1** garde la connexion ouverte et rend `Host` obligatoire, mais traite les requêtes une à une : une réponse lente bloque les suivantes. **HTTP/2** passe au binaire et multiplexe les requêtes sur une seule connexion, mais une perte de paquet TCP bloque encore tous les streams. **HTTP/3** remplace TCP par QUIC : un handshake plus court, des pertes isolées par stream et une connexion qui survit au changement de réseau. Derrière chaque version, une seule obsession : faire perdre moins d'allers-retours au client. Les mots du cours **ALPN** : extension de TLS qui choisit le protocole (`h2`, `http/1.1`) pendant la négociation du chiffrement. **Blocage en tête de file** : un élément lent ou perdu bloque tous ceux qui le suivent. **Frame** : petit bloc binaire de HTTP/2 et HTTP/3 (`HEADERS`, `DATA`…). **Handshake** : échange initial qui ouvre une connexion (TCP, TLS, QUIC). **HPACK, QPACK** : compression des en-têtes de HTTP/2 et de HTTP/3. **Multiplexing** : plusieurs échanges en même temps sur un même canal. **Pseudo-en-tête** : en-tête spécial de HTTP/2 et HTTP/3 qui commence par `:` (`:method`, `:path`). **QUIC** : transport sur UDP, avec TLS 1.3 intégré et des streams indépendants. **RTT** : temps d'un aller-retour sur le réseau. **Stream** : un échange requête-réponse, identifié par un numéro. **TCP** : transport fiable et ordonné, avec connexion. **TLS, HTTPS** : le chiffrement de la connexion, et HTTP par-dessus TLS. **UDP** : transport léger, sans connexion ni garantie. Pour aller plus loin Construire un serveur web from scratch, partie 1 : accepter des connexions TCP RFC 9110 : la sémantique de HTTP, commune à toutes les versions RFC 1945 : HTTP/1.0 RFC 9112 : HTTP/1.1 RFC 9113 : HTTP/2 RFC 9000 : QUIC RFC 9114 : HTTP/3