FlowingDev

Messages HTTP, déconstruits : comment fabriquer les paquets bruts du Web

Plongez dans l'anatomie des messages HTTP bruts, de la ligne de départ et des headers jusqu'aux corps 'multipart' les plus complexes, et comprenez enfin comment les clients et serveurs web se parlent.

Essayer l'outil: Constructeur de messages HTTP

En une phrase

Un message HTTP est un bloc de texte brut formaté que les navigateurs et les serveurs web s'échangent, qui sert à la fois d'étiquette d'expédition, de manuel d'instructions et de contenu du colis pour chaque interaction sur le web.

Le problème que ça résout

Dans la soupe primordiale du web du début des années 90, les choses étaient simples. Un navigateur avait besoin d'un moyen de demander à un serveur « Dis, je peux avoir ce fichier science.html ? » et le serveur avait besoin d'un moyen de répondre « Bien sûr, le voilà » ou « Désolé, pas trouvé ». Cette conversation avait besoin de règles — un protocole. Ce protocole est devenu HTTP, le Hypertext Transfer Protocol.

Le « problème » qu'il a résolu était de créer un langage universel et sans ambiguïté pour le web. Sans un format standard, un serveur pourrait s'attendre à recevoir la requête sur une seule ligne, tandis qu'un autre pourrait exiger un haïku. Ça aurait été le chaos. Le premier HTTP/0.9 était d'une simplicité enfantine : GET /la-page-que-je-veux.html. Le serveur renvoyait juste le HTML en vrac.

Mais le web n'est pas resté simple. On a eu besoin d'envoyer des données au serveur pour remplir des formulaires. On a eu besoin de gérer différents types de contenu comme les images et, plus tard, le JSON. On a eu besoin de sécurité, de mise en cache, et d'un moyen pour les navigateurs de se décrire. La simple requête d'une ligne a évolué en un « message » structuré en plusieurs parties, avec une ligne de départ, un bloc de métadonnées (les headers) et un corps optionnel pour le payload. Fabriquer ces messages à la main est devenu la compétence fondamentale pour quiconque travaille directement avec l'infrastructure web, les API ou la sécurité, résolvant le problème de comment mener des affaires de plus en plus complexes via le simple dialogue requête-réponse du web.

Comment ça marche sous le capot

À la base, un message HTTP, c'est juste du texte. On pourrait littéralement en taper un dans un terminal et le « piper » vers un serveur si le cœur nous en disait. Ce texte est divisé en trois parties : une ligne de départ, un bloc de headers et un corps optionnel, le tout séparé par des sauts de ligne spécifiques (\r\n, ou CRLF pour « Carriage Return, Line Feed »).

Il existe deux types de messages : les requêtes (du client au serveur) et les réponses (du serveur au client). Ils sont presque identiques mais ont une première ligne différente.

Anatomie d'un message de requête

C'est votre navigateur qui demande quelque chose.

GET /documentation/guides/http-builder HTTP/1.1
Host: flowing.dev
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0
Accept: text/html,*/*
Accept-Language: en-US,en;q=0.5
Connection: keep-alive

<-- Le corps irait ici, mais les requêtes GET n'en ont généralement pas -->
  1. La ligne de départ : GET /documentation/guides/http-builder HTTP/1.1

    • GET : La méthode HTTP (ou verbe). C'est ce que vous voulez faire. GET récupère des données, POST soumet de nouvelles données, PUT met à jour des données existantes, DELETE supprime des données.
    • /documentation/... : Le chemin de la ressource. Combiné avec le header Host, cela forme l'URL complète.
    • HTTP/1.1 : La version du protocole.
  2. Les headers : Une liste de paires clé-valeur qui fournissent des métadonnées cruciales sur la requête.

    • Host: flowing.dev : À qui s'adresse cette requête ? Ce header est obligatoire en HTTP/1.1.
    • User-Agent: Mozilla/5.0... : Qui envoie cette requête ? Le navigateur s'identifie.
    • Accept: text/html,*/* : Quel genre de format de réponse puis-je comprendre ? Ici, le navigateur préfère le HTML mais acceptera n'importe quoi.
  3. La ligne vide : Après le dernier header, une unique ligne vide (\r\n) signale « les headers sont finis, le corps arrive ». C'est non négociable. L'omettre cassera tout.

  4. Le corps : Le payload de données. Pour une requête GET, il est généralement vide. Pour un POST ou un PUT, c'est ici que vivent vos données de formulaire ou votre payload JSON.

Anatomie d'un message de réponse

C'est le serveur qui répond à la requête.

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 15328
Server: Vercel
Date: Mon, 25 Sep 2023 10:30:00 GMT
Cache-Control: public, max-age=0, must-revalidate

<!DOCTYPE html>
<html>
  <head>...</head>
  <body>...</body>
</html>
  1. La ligne de statut : HTTP/1.1 200 OK

    • HTTP/1.1 : La version du protocole, la même que pour la requête.
    • 200 : Le Code de statut. Un nombre à trois chiffres résumant le résultat. 2xx signifie succès, 3xx signifie redirection, 4xx signifie que vous (le client) avez fait une erreur, et 5xx signifie que moi (le serveur) j'ai fait une erreur.
    • OK : Le Message de statut. Un résumé lisible du code de statut.
  2. Les headers : Des métadonnées sur la réponse.

    • Content-Type: text/html : « Le corps que je t'envoie est du HTML. » C'est essentiel pour que le navigateur sache comment afficher le payload.
    • Content-Length: 15328 : « Le corps fait exactement 15 328 octets. »
    • Set-Cookie: ... : Comment les serveurs disent aux navigateurs de stocker des cookies.
    • Cache-Control: ... : Des instructions sur la façon dont le navigateur ou les proxys intermédiaires doivent mettre en cache cette réponse.
  3. Le corps : La ressource que le client a demandée — HTML, CSS, un objet JSON, des données d'image, etc.

La foire au corps : encoder le payload

Quand une requête a un corps, elle a besoin d'un header Content-Type pour expliquer son format. Les trois plus courants sont :

  • application/x-www-form-urlencoded : Le format par défaut des formulaires HTML à l'ancienne. C'est juste une chaîne de requête dans le corps.

    name=Grace+Hopper&title=Rear+Admiral
    
  • application/json : Le roi des API modernes. Le corps est une chaîne JSON.

    {
      "name": "Grace Hopper",
      "title": "Rear Admiral"
    }
    
  • multipart/form-data : Le format pour soumettre des formulaires qui incluent des téléversements de fichiers. C'est comme un message dans un message. Le corps est divisé en parties, chacune séparée par une chaîne de délimitation (« boundary »). Chaque partie peut avoir ses propres mini-headers (comme Content-Disposition et Content-Type) et son propre contenu.

    POST /profiles/edit HTTP/1.1
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
    
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="username"
    
    ada_lovelace
    ----WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="avatar"; filename="portrait.jpg"
    Content-Type: image/jpeg
    
    <...les données binaires brutes de l'image vont ici...>
    ----WebKitFormBoundary7MA4YWxkTrZu0gW--
    

Histoires vécues

L'affaire du Content-Type manquant

Un développeur créait sa première API REST. L'endpoint était censé accepter un payload JSON pour créer un nouvel utilisateur. Il a écrit le code serveur et l'a testé avec un outil en ligne de commande, envoyant un objet JSON parfaitement valide. Mais le serveur continuait de répondre avec 400 Bad Request. Il a passé deux heures à fixer son JSON, persuadé d'avoir oublié une virgule. En désespoir de cause, il a demandé de l'aide à un dev senior. Le dev senior a jeté un œil à la requête et a demandé : « Il est où, ton header Content-Type ? » Le développeur avait envoyé les données JSON, mais il n'avait jamais dit au serveur que c'était du JSON. Le framework du serveur, s'attendant au format par défaut x-www-form-urlencoded, a essayé de parser le JSON comme une chaîne de requête, a lamentablement échoué et a rejeté la requête.

Leçon : Le corps d'un message n'a aucun sens sans le header Content-Type pour lui donner un contexte. Il faut étiqueter son colis correctement.

L'embrouille du multipart

Une équipe créait une page de « paramètres » où un utilisateur pouvait changer son nom et optionnellement téléverser une nouvelle photo de profil. Le dev frontend junior a implémenté cela avec deux appels API distincts : une requête PUT avec le nom de l'utilisateur dans un corps JSON, et ensuite, si une image était sélectionnée, une requête POST avec les données de l'image. Ça fonctionnait, mais c'était bancal et ça créait des « race conditions ». Et si le changement de nom réussissait mais que le téléversement de l'image échouait ? L'utilisateur se retrouverait dans un état incohérent. Un ingénieur backend a vu le trafic réseau et l'a pris à part. « C'est un cas d'usage parfait pour multipart/form-data », a-t-elle expliqué. Ils ont refactorisé le code pour construire une seule requête POST avec deux parties : une pour le champ du nom et une pour le fichier image. Cela a simplifié le code et a rendu toute la mise à jour une opération atomique.

Leçon : multipart n'est pas réservé qu'aux fichiers. C'est fait pour envoyer un assortiment de données hétéroclites (champs de texte, fichiers, différents types de contenu) en une seule requête fiable.

Le fantôme dans le cache

Un site d'e-commerce organisait une vente flash, mais les utilisateurs se plaignaient de voir des prix obsolètes. L'équipe ops était perplexe ; leur cache côté serveur était configuré correctement. On a fait appel à une experte en performance web. Au lieu d'utiliser les outils de développement du navigateur, elle a utilisé un outil pour inspecter la réponse HTTP brute d'une page produit. Elle a trouvé le coupable instantanément. Un « load balancer » mal configuré devant les serveurs web injectait son propre header Cache-Control: public, max-age=3600, écrasant le header Cache-Control: no-cache prévu par le serveur. Ce header indésirable disait aux navigateurs et aux CDN de mettre en cache les prix pendant une heure, peu importe ce que disait le serveur d'application.

Leçon : Le message HTTP brut est la source de vérité ultime. Les outils de haut niveau peuvent parfois cacher ou mal interpréter des détails qui sont pourtant clairs comme le jour dans le texte lui-même.

Erreurs et pièges courants

  • Oublier la ligne vide. Un message HTTP doit avoir un CRLF (\r\n) entre les headers et le corps. S'il manque, les parseurs penseront que votre corps est juste un autre header malformé et la requête échouera.
  • Content-Length incohérent. Si vous déclarez un header Content-Length, sa valeur doit être la taille exacte en octets du corps. Si elle est trop petite, vos données seront tronquées. Si elle est trop grande, le serveur attendra éternellement des octets qui n'arriveront jamais.
  • Mauvais Content-Type. Envoyer un corps JSON en l'étiquetant comme text/plain est la recette pour une erreur 4xx. Le header et le corps doivent être en accord.
  • CRLF vs. LF. La spécification officielle exige \r\n pour les sauts de ligne. La plupart des serveurs modernes sont tolérants et accepteront un simple \n (Line Feed). Cependant, compter là-dessus peut faire échouer votre requête avec des serveurs, proxys ou firewalls plus anciens et plus stricts.
  • Encoder les caractères spéciaux. Oublier d'encoder en URL les données dans une chaîne de requête ou un corps x-www-form-urlencoded est un bug classique. Un espace doit devenir %20, un & doit devenir %26, et ainsi de suite, ou vous risquez de corrompre vos données.

Pourquoi vous devriez vous y intéresser

La plupart du temps, votre navigateur, framework ou librairie (comme axios ou requests) gère les détails sordides de la construction des messages HTTP pour vous. Mais vous devriez savoir le faire à la main quand :

  • Vous êtes en pleine séance de débogage. Quand un appel API ne fonctionne pas et que le message d'erreur est vague, inspecter ou recréer le message HTTP brut est le juge de paix. Cela vous permet de voir exactement ce qui transite sur le réseau, sans aucune couche d'abstraction.
  • Vous construisez ou testez une API. Comprendre la structure des messages est fondamental pour concevoir de bons endpoints d'API et écrire des tests d'intégration efficaces. Les testeurs de sécurité passent leurs journées à fabriquer des messages mal formés pour trouver des vulnérabilités.
  • Vous scrapez un site web. Pour imiter avec succès un vrai navigateur et contourner les mesures anti-bots, vous devez souvent construire une requête avec une combinaison très spécifique de headers (User-Agent, Referer, Accept-*, etc.).
  • Vous travaillez avec des webhooks. Quand votre application reçoit un webhook d'un service comme Stripe ou GitHub, vous êtes le destinataire d'une requête HTTP brute. Vous devrez parser ses headers (par exemple, pour les signatures de sécurité) et son corps pour agir sur l'événement.

Savoir assembler un message HTTP de A à Z, c'est comme pour un mécanicien de connaître le fonctionnement d'un moteur à combustion interne. On ne le fait pas tous les jours, mais quand quelque chose déraille, cette connaissance fondamentale n'a pas de prix.

Pour aller plus loin

Théorie bouclée. Place à la pratique — 100 % dans ton navigateur.

Essayer l'outil: Constructeur de messages HTTP