SSD Nodes Learn 🎉 VPS dès $4.99/mois
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-07

HTTP : guide pratique pour administrer un serveur web

Comprenez HTTP côté serveur : méthodes, codes 200 et 404, en-têtes utiles, journaux nginx, et la place de TLS et HTTP/3 dans vos diagnostics.

Qu’est-ce que HTTP ?

HTTP (Hypertext Transfer Protocol) est l’ensemble des règles qu’un client et un serveur web utilisent pour demander une ressource et la renvoyer. Le client envoie une requête : une méthode telle que GET, un chemin tel que /pricing, une version du protocole, une liste d’en-têtes et parfois un corps. Le serveur répond avec un code d’état tel que 200, suivi de ses propres en-têtes et généralement d’un corps. Chaque affichage de page et chaque appel d’API (application programming interface) sur votre serveur correspond à cet échange, répété.

HTTP ne conserve aucun état par lui-même. Le serveur ne se souvient pas de ce que vous avez demandé une seconde plus tôt. Tout ce qui se comporte comme une mémoire, par exemple une session de connexion, est donc transporté dans un en-tête à chaque requête. Cette propriété explique une grande partie de ce qui suit : la mise en cache dépend entièrement des en-têtes, et un load balancer peut envoyer votre requête suivante vers un autre backend sans rien interrompre.

Tout ce qui suit montre à quoi ressemble ce modèle côté serveur, dans votre access log et dans votre configuration nginx.

Une requête et une réponse brutes, annotées

Voici une requête HTTP/1.1 complète. Une ligne vide termine les en-têtes, et tout ce qui suit cette ligne constitue le corps. Une GET n’a normalement pas de corps.

GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzip
  • GET est la méthode. Elle indique l’opération à effectuer. GET lit, POST envoie des données, PUT remplace, DELETE supprime et HEAD demande les en-têtes d’une GET sans son corps.
  • /pricing est le chemin. Le nom d’hôte ne fait pas partie de la ligne de requête, ce qui explique la présence de l’en-tête suivant.
  • HTTP/1.1 est la version du protocole utilisée par le client.
  • Host: example.com indique le site demandé par le client. HTTP/1.1 l’exige. Nginx répond donc par 400 Bad Request si la requête n’en contient pas.
  • Les autres champs correspondent aux préférences. Accept-Encoding: gzip indique que le client peut décompresser le contenu. Le serveur peut donc compresser le corps.

La réponse suit la même structure, avec une ligne de statut en tête.

HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300

<!doctype html>...
  • 200 OK est le code de statut suivi de sa raison. Le code est l’information importante. La raison est décorative et les clients l’ignorent.
  • Content-Type indique au client comment traiter les octets qui suivent.
  • Content-Length est la taille du corps en octets. Le client sait ainsi où le corps se termine. Lorsque sa taille n’est pas connue à l’avance, le serveur envoie Transfer-Encoding: chunked et indique la fin avec un chunk de longueur nulle.
  • Cache-Control indique au navigateur et aux caches intermédiaires pendant combien de temps ils peuvent conserver cette réponse.
  • Dans les deux sens, la ligne vide qui suit les en-têtes les sépare du corps.

Les noms d’en-tête ne sont pas sensibles à la casse. Chaque ligne se termine par un retour chariot suivi d’un saut de ligne, et non par un simple saut de ligne. Vous ne les saisirez pas à la main, mais vous les rencontrerez dans une capture de paquets.

Pour observer un échange réel, exécutez cette commande contre un site dont vous êtes propriétaire :

curl -sS -o /dev/null -D - https://example.com/

-D - écrit les en-têtes de réponse dans votre terminal et -o /dev/null ignore le corps. Préférez cette option à curl -I, car -I envoie une requête HEAD. Un serveur d’application qui traite HEAD différemment de GET, comme c’est souvent le cas, vous montrera alors des en-têtes qu’aucun navigateur ne reçoit. curl -v affiche les deux côtés, en marquant les lignes de requête avec > et les lignes de réponse avec <.

À quoi ressemble la ligne de requête dans votre access log nginx

nginx fournit un format de log combined, dont voici la définition :

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

Une ligne produite par ce format :

203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"
  • 203.0.113.45 correspond à $remote_addr, l’adresse qui a ouvert la connexion TCP (transmission control protocol). Derrière un proxy, il s’agit de l’adresse du proxy, pas de celle du visiteur.
  • Le premier - est un placeholder fixe. Le second correspond à $remote_user, qui n’est renseigné que si l’authentification HTTP basic est utilisée.
  • "GET /pricing HTTP/1.1" correspond à $request, la ligne de requête recopiée exactement telle qu’elle est arrivée.
  • 200 correspond au code de statut renvoyé par votre serveur, et non à celui perçu par le visiteur.
  • 5310 correspond à $body_bytes_sent, le body uniquement. Les en-têtes de réponse ne sont pas comptés ; ce nombre est donc toujours inférieur au nombre d’octets réellement envoyés.
  • Les deux derniers champs entre guillemets correspondent à Referer et User-Agent. Ils viennent tous deux du client et peuvent donc contenir n’importe quoi.

Comme $request est recopié verbatim, les données incorrectes apparaissent telles quelles. Un client qui parle TLS (transport layer security) à votre port 80 en plaintext laisse une ligne 400 dont le champ de requête commence par des octets échappés tels que "\x16\x03\x01\x02\x00\x01". \x16 correspond au type d’enregistrement du handshake TLS. Ces octets sont donc le début d’un ClientHello, et non une ligne de requête. Votre serveur fonctionne correctement. Un élément tente d’utiliser HTTPS sur un port HTTP.

Ajoutez également $server_protocol à votre format de log. Il affiche HTTP/1.1, HTTP/2.0 ou HTTP/3.0. C’est le moyen le plus rapide de vérifier qu’un changement de protocole a bien été pris en compte.

Signification des codes d’état courants renvoyés par votre propre site

Le premier chiffre indique la classe. C’est cette classe qu’il faut lire en premier.

2xx signifie que la requête a abouti. 200 OK correspond à une lecture normale. 201 Created est renvoyé après un POST qui a créé une ressource. 204 No Content indique que la requête a abouti sans contenu à renvoyer. C’est la réponse habituelle à un DELETE.

3xx signifie qu’il faut consulter une autre URL. 301 indique une redirection permanente. Les navigateurs la mettent fortement en cache, parfois jusqu’à ce que l’utilisateur efface son profil. Un 301 qui pointe vers le mauvais nom d’hôte est donc difficile à corriger. Utilisez 302 pendant les tests de redirection. 304 Not Modified indique une réussite, pas une erreur : le client a envoyé If-None-Match avec un ETag (entity tag) que vous reconnaissez toujours. Vous avez donc répondu avec les en-têtes, sans corps de réponse. Une journalisation remplie de 304s signifie que la mise en cache fonctionne.

4xx signifie que la requête était incorrecte. 400 Bad Request indique une entrée mal formée. 401 Unauthorized signifie réellement que le client n’est pas authentifié et la réponse doit contenir un en-tête WWW-Authenticate indiquant le schéma utilisé. 403 Forbidden signifie que la requête a été comprise, mais refusée malgré tout. 404 Not Found correspond à un chemin qui n’existe pas. 405 Method Not Allowed indique que le chemin est correct, mais que la méthode ne l’est pas. C’est ce que renvoie un POST vers un emplacement de fichiers statiques. 413 indique que le corps de la requête dépasse client_max_body_size de nginx, qui vaut 1 megabyte par défaut. Le journal d’erreurs le confirme avec client intended to send too large body.

Un 403 sur un fichier statique concerne presque toujours le système de fichiers, et non une règle HTTP. Consultez /var/log/nginx/error.log avant de modifier la configuration. open() "/srv/site/index.html" failed (13: Permission denied) signifie que l’utilisateur du worker nginx ne peut pas lire le fichier. La cause la plus fréquente est l’absence de permission d’exécution pour les autres utilisateurs sur un répertoire parent. directory index of "/srv/site/" is forbidden signifie que le chemin correspond à un répertoire sans fichier d’index, alors que autoindex est désactivé.

5xx signifie qu’un problème s’est produit de votre côté. 500 indique une erreur non gérée dans votre application. 502 Bad Gateway signifie que nginx n’a pas pu obtenir une réponse utilisable de l’upstream. Le journal d’erreurs en indique la cause : connect() failed (111: Connection refused) while connecting to upstream signifie que rien n’écoute sur l’adresse indiquée dans proxy_pass. 504 Gateway Timeout signifie que l’upstream a accepté la connexion, puis n’a rien envoyé pendant proxy_read_timeout, soit 60 secondes par défaut. Le journal l’enregistre sous la forme upstream timed out (110: Connection timed out) while reading response header from upstream. 503 Service Unavailable indique un refus volontaire. Notez que le rate limiter intégré à nginx renvoie 503, car limit_req_status vaut 503 par défaut. Si vous recherchez 429 Too Many Requests dans votre journal, mais trouvez 503 à la place, c’est la raison. Définissez limit_req_status 429; pour obtenir le code correct.

Les en-têtes importants lors de l’exécution du serveur

Host sélectionne le site. Une seule adresse IP peut servir des centaines de noms d’hôte, et nginx compare Host à server_name pour déterminer quel bloc server doit répondre. Si aucun nom ne correspond, nginx utilise le serveur par défaut. Il s’agit du premier bloc à l’écoute sur cette adresse et ce port, sauf si un autre bloc est marqué default_server. Si un nouveau virtual host renvoie le mauvais site, c’est presque toujours pour cette raison : le nom ne correspondait pas et la requête est tombée sur le serveur par défaut. Testez-le sans modifier le DNS :

curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/

User-Agent est une description du client, fournie librement sous forme de texte. Utilisez-la comme indication lors de la lecture des journaux. Ne l’utilisez jamais comme mécanisme de contrôle, car un client qui veut mentir à ce sujet le fera. Bloquer un scraper avec User-Agent ne filtre donc que les clients suffisamment polis pour s’identifier honnêtement.

Content-Type détermine comment les octets sont interprétés : application/json pour une requête d’API, text/html; charset=utf-8 pour une page. nginx associe les extensions de fichiers aux types avec /etc/nginx/mime.types, et le fichier nginx.conf fourni par le paquet définit default_type application/octet-stream;. Ainsi, un fichier dont l’extension n’est pas connue de nginx est proposé au téléchargement au lieu d’être affiché. Le symptôme visible est une page qui se charge sans mise en forme, tandis que la console du navigateur affiche Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type. MIME signifie ici multipurpose internet mail extensions, le système de nommage à l’origine de ces chaînes de type.

Cache-Control permet de contrôler tous les caches situés entre votre serveur et le lecteur. public, max-age=31536000, immutable convient aux ressources dont le nom contient un hash du contenu, car le nom change lorsque le contenu change. no-store doit être utilisé pour tout contenu spécifique à un utilisateur, car un cache partagé qui conserve une page avec une session ouverte la transmettra à la personne suivante qui demande la même URL. private est le réglage intermédiaire : le navigateur peut conserver la ressource, mais pas un cache partagé.

X-Forwarded-For est nécessaire parce qu’un proxy masque l’adresse du visiteur. Une fois la requête passée par un reverse proxy, $remote_addr contient l’adresse du proxy. Vos journaux, votre géolocalisation et votre rate limiting ne voient donc qu’un seul client. Le proxy doit transmettre l’adresse d’origine :

proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Il faut ensuite indiquer au serveur récepteur qu’il doit faire confiance à cette information, et lui préciser exactement à qui faire confiance :

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;

Ne répertoriez que les plages que vous contrôlez. X-Forwarded-For est du texte brut qu’un client peut envoyer librement, et set_real_ip_from 0.0.0.0/0; permet donc à un visiteur de choisir l’adresse enregistrée dans vos journaux et l’adresse prise en compte par votre rate limiter.

X-Forwarded-Proto évite un problème précis et très fréquent. Votre proxy termine TLS, puis transmet la requête à l’application en HTTP simple. L’application voit une requête HTTP simple, considère que le visiteur doit utiliser HTTPS et répond 301 https://example.com/. Le navigateur suit cette redirection, le proxy termine de nouveau TLS et transmet encore la requête en HTTP simple. La boucle se répète jusqu’à ce que le navigateur abandonne avec ERR_TOO_MANY_REDIRECTS. L’envoi de X-Forwarded-Proto: https indique à l’application que le visiteur utilise déjà HTTPS. Elle cesse alors de rediriger.

HTTP/1.1, HTTP/2 et HTTP/3 : ce qui change pour vous

HTTP/1.1 est un protocole texte et traite une seule requête à la fois par connexion. Connection: keep-alive permet à la requête suivante de réutiliser la même connexion TCP, ce qui évite le coût de son établissement, mais les réponses reviennent toujours dans l’ordre des requêtes. Une réponse lente bloque tout ce qui est en attente derrière elle. C’est le blocage en tête de ligne, et les navigateurs le contournent en ouvrant plusieurs connexions vers le même nom d’hôte.

HTTP/2 conserve les mêmes méthodes et les mêmes codes d’état, mais utilise un framing binaire. De nombreuses requêtes partagent une seule connexion sous forme de streams indépendants, et le texte répété des en-têtes est compressé, ce qui compte car une requête moderne en contient beaucoup. La connexion reste basée sur TCP : la perte d’un paquet bloque donc tous les streams de cette connexion jusqu’à l’arrivée de sa retransmission. Le blocage en tête de ligne n’a pas disparu. Il a été déplacé de HTTP vers la couche transport. Le server push faisait partie de HTTP/2, mais il a disparu en pratique, car Chrome a supprimé sa prise en charge en 2022.

HTTP/3 conserve une nouvelle fois les mêmes sémantiques et remplace TCP par QUIC, un transport basé sur UDP (user datagram protocol). Les streams QUIC sont indépendants jusqu’à la couche la plus basse, si bien qu’un paquet perdu ne bloque que le stream auquel il appartenait. TLS 1.3 est intégré à la négociation QUIC au lieu d’être superposé, ce qui réduit le nombre d’allers-retours nécessaires à l’établissement d’une nouvelle connexion. Deux conséquences pratiques en découlent : le port UDP 443 doit être ouvert dans chaque firewall traversé, et tout réseau qui limite ou bloque UDP fera repasser les clients sur HTTP/2.

Ce qui change concrètement pour vous. Les navigateurs ne démarrent jamais directement en HTTP/3. Ils se connectent en HTTP/2 ou en HTTP/1.1, voient un en-tête Alt-Svc: h3=":443"; ma=86400 dans la réponse, puis utilisent HTTP/3 lors des connexions suivantes vers cet hôte. Cet en-tête n’est donc pas un élément décoratif facultatif. C’est le mécanisme de découverte. Dans nginx, HTTP/2 est devenu une directive distincte dans la version 1.25.1 (http2 on; dans le bloc server, en remplacement de l’ancien paramètre listen ... http2), et QUIC est arrivé dans la branche mainline 1.25.0, où un site HTTP/3 nécessite listen 443 quic reuseport; en plus du listen 443 ssl; habituel.

Les proxys n’ont pas tous le même niveau de maturité sur ce point. Vérifiez-le avec la version que vous utilisez réellement. En août 2026, Caddy sert HTTP/3 par défaut, sans configuration supplémentaire. nginx nécessite explicitement le listener quic ainsi que l’en-tête Alt-Svc décrit plus haut. Traefik l’active par entry point au moyen d’une option http3 explicite. Si vous terminez TLS avec Traefik devant plusieurs applications Docker, la version du protocole reçue par vos visiteurs est déterminée à cet endroit, tandis que la liaison entre le proxy et votre conteneur utilise généralement HTTP/1.1 en clair, quelle que soit la version négociée par le navigateur.

Vérifiez au lieu de supposer. curl --http3 -sS -o /dev/null -D - https://example.com/ ne fonctionne que si curl -V indique HTTP3 parmi ses fonctionnalités, et la plupart des builds fournis par les distributions ne l’incluent pas. La vérification la plus fiable consiste à utiliser vos propres journaux : ajoutez $server_protocol au format, puis observez ce que négocient les navigateurs réels. Avant cela, confirmez que le port UDP 443 est réellement ouvert, car un firewall qui autorise uniquement TCP 443 fera échouer HTTP/3 discrètement, tandis que le site continuera de fonctionner en HTTP/2. Savoir quels ports sont ouverts et en écoute sur votre serveur Linux est le premier point à vérifier.

HTTPS : HTTP est le protocole, TLS est l’enveloppe

HTTPS n’est pas un protocole distinct. Il s’agit des mêmes requêtes et des mêmes codes d’état, transportés dans une session TLS. Le port 80 les transporte en clair, tandis que le port 443 les transporte de manière chiffrée. La négociation TLS se termine d’abord, puis la requête HTTP circule dans le canal chiffré. C’est pourquoi un problème de certificat n’est jamais associé à un code d’état : l’échec se produit avant l’envoi du moindre octet HTTP, donc aucune réponse ne peut recevoir de code.

Un détail dans l’ordre des opérations est important sur un serveur qui héberge plusieurs sites. Le certificat est sélectionné à l’aide de SNI (server name indication), un champ de la négociation TLS qui contient le nom d’hôte en clair avant même l’existence d’un en-tête HTTP. Le serveur sélectionne donc d’abord un certificat à partir de SNI, puis sélectionne un virtual host à partir de l’en-tête Host. Il s’agit de deux recherches distinctes, qui correspondent normalement. Lorsqu’elles ne correspondent pas, le navigateur affiche une erreur de nom, telle que NET::ERR_CERT_COMMON_NAME_INVALID, et n’envoie aucune requête, car le certificat de votre serveur par défaut a été présenté pour un nom qu’il ne couvre pas.

Pour un site public, obtenez un certificat valide et laissez-le se renouveler automatiquement. Certbot avec Let's Encrypt sur nginx écrit les chemins des certificats dans votre server block et installe le timer de renouvellement à votre place. Pour un nom d’hôte qu’aucune autorité publique ne peut valider, par exemple un nom interne ou une adresse IP privée sur votre propre réseau, un certificat autosigné sur Ubuntu est l’option la plus appropriée, à condition d’accepter que chaque client doive être configuré pour lui faire confiance.

Une fois TLS opérationnel, redirigez tout le trafic du port 80 vers le port 443 :

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Ajoutez Strict-Transport-Security uniquement lorsque vous en êtes certain. L’en-tête add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; indique aux navigateurs de refuser le HTTP en clair pour ce nom d’hôte pendant deux ans. Ils appliquent cette règle depuis leur propre cache. Retirer l’en-tête ultérieurement ne l’annule donc pas. Commencez avec un max-age de quelques heures. Vérifiez que chaque sous-domaine utilise bien HTTPS, puis augmentez cette valeur.

FAQ

Quelle est la différence entre HTTP et HTTPS ?

HTTPS est du HTTP transmis dans une session TLS (transport layer security). Les méthodes et les codes d’état sont identiques. Ce qui change, c’est que les octets sont chiffrés entre le client et le composant qui termine TLS, et que le port par défaut passe de 80 à 443. Comme le handshake TLS se termine avant l’envoi du premier octet HTTP, un échec de certificat ne produit jamais de code d’état. C’est pourquoi l’avertissement de certificat d’un navigateur affiche un nom d’erreur tel que NET::ERR_CERT_COMMON_NAME_INVALID, et non un nombre comme 403.

Pourquoi mon site renvoie-t-il 502 Bad Gateway ?

Un 502 de nginx signifie que nginx n’a pas pu obtenir de réponse exploitable de l’upstream vers lequel il fait office de proxy. La requête du visiteur était donc correcte, mais un composant situé derrière nginx ne l’était pas. Consultez /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream signifie que rien n’écoute sur l’adresse et le port indiqués dans proxy_pass. Vérifiez donc que l’application fonctionne et écoute à l’endroit prévu. no live upstreams while connecting to upstream signifie que tous les serveurs du bloc upstream ont été marqués comme indisponibles après plusieurs échecs. Comparez avec 504 Gateway Timeout, qui signifie que l’upstream a bien accepté la connexion, puis n’a pas répondu dans le délai proxy_read_timeout.

Pourquoi mon access log affiche-t-il la même adresse IP pour tous les visiteurs ?

Parce que $remote_addr enregistre l’adresse qui a ouvert la connexion TCP. Derrière un reverse proxy ou un content delivery network, cette adresse est celle du proxy. L’adresse du visiteur se trouve plutôt dans l’en-tête X-Forwarded-For. Configurez proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; sur le proxy, puis configurez set_real_ip_from sur nginx, côté réception, avec la plage d’adresses du proxy, ainsi que real_ip_header X-Forwarded-For;. Ne répertoriez que les plages que vous contrôlez, car cet en-tête est du texte que n’importe quel client peut envoyer. Lui faire confiance depuis l’ensemble d’Internet permettrait donc à un visiteur de choisir l’adresse que vous journalisez et celle à laquelle vous appliquez la limitation de débit.

Dois-je activer HTTP/2 ou HTTP/3 ?

HTTP/2 vaut la peine d’être activé. Il s’agit d’une seule directive sur un site qui utilise déjà TLS, et il supprime la limite de requêtes par connexion qui ralentit les pages contenant de nombreux petits fichiers. Le gain de HTTP/3 est plus limité et moins certain. Il nécessite aussi un port UDP 443 ouvert et une version du proxy compilée avec la prise en charge de QUIC. N’oubliez pas que les navigateurs passent à HTTP/3 uniquement après avoir vu un en-tête Alt-Svc dans une réponse précédente. Sans cet en-tête, rien ne change, quelle que soit la valeur de votre directive listen. Ajoutez $server_protocol à votre format de log et mesurez le protocole effectivement négocié par les visiteurs avant d’y consacrer du temps.

Que signifie 403 Forbidden lorsque le fichier existe ?

Sur un site statique, un 403 indique généralement un problème de permissions sur le système de fichiers, et non une règle HTTP. open() ... failed (13: Permission denied) dans /var/log/nginx/error.log signifie que l’utilisateur du worker nginx ne peut pas lire le fichier. La cause la plus fréquente est l’absence de permission d’exécution pour les autres utilisateurs sur un répertoire parent, plutôt qu’un problème de mode sur le fichier lui-même. directory index of ... is forbidden signifie que la requête correspond à un répertoire sans fichier d’index alors que autoindex est désactivé. Une règle deny explicite dans le bloc location correspondant renvoie également 403. Consultez donc ce bloc lorsque l’error log ne fournit aucune information.

#http#https#web-server#headers#http3