Suivre une requête réseau : DNS, connexions, TLS et HTTP
Lorsqu’un site ne se charge pas, cherchez jusqu’où la requête est arrivée : le nom d’hôte a-t-il été résolu, la connexion établie, la négociation TLS réussie, une réponse HTTP reçue ? Un code 404 et un port injoignable appellent des recherches différentes. La requête peut aussi passer par un proxy : le serveur auquel le navigateur se connecte n’est alors pas forcément le programme qui réalise l’opération métier.
Prenons l’URL hypothétique https://api.example.com:443/health?full=1 pour suivre ce parcours. https indique un accès HTTP sécurisé, api.example.com est le nom d’hôte, 443 le port, /health le chemin et full=1 la chaîne de requête. Sans port explicite, HTTPS utilise par défaut 443, et HTTP 80. Cette convention vient du schéma de l’URL ; les enregistrements DNS A/AAAA ne transportent pas ces ports. RFC 9110 §4.2
1. Résoudre le nom d’hôte et comprendre le cache
Le DNS associe les noms de domaine à des enregistrements typés. L’application s’adresse généralement à l’interface de résolution du système, qui peut transmettre la demande à un résolveur récursif configuré. Si les informations locales ne suffisent pas, ce résolveur suit les délégations dans l’arbre des domaines jusqu’aux serveurs faisant autorité pour les données recherchées. Une réponse déjà en cache peut éviter une partie de ces échanges. RFC 1034 §2.4 et §5.3
Pour rechercher une adresse, un enregistrement A fournit une adresse IPv4, et un enregistrement AAAA une adresse IPv6, définie dans la RFC 3596 §2. Un même nom d’hôte peut avoir plusieurs adresses. Il faut ensuite tenter une connexion pour savoir si une adresse est joignable depuis le réseau actuel.
Le TTL, ou durée de vie, se mesure en secondes et indique normalement combien de temps un enregistrement en cache peut être utilisé avant de consulter à nouveau sa source. Il s’applique aux copies conservées, sans imposer que tous les clients changent d’adresse au même instant. RFC 1034 §3.6 Un résolveur configuré pour servir des données périmées peut continuer à renvoyer des enregistrements expirés lorsqu’il ne parvient pas à les rafraîchir auprès des serveurs faisant autorité. RFC 8767 §4
Supposons qu’un résolveur mette l’ancienne adresse en cache à 12:00:00 avec un TTL de 300 secondes. À 12:01:00, le serveur faisant autorité change l’adresse et attribue au nouvel enregistrement un TTL de 60 secondes. Le résolveur ne rafraîchit pas sa copie à l’avance et n’applique aucune politique supplémentaire de service des données expirées :
Réduire le TTL au moment du changement ne raccourcit pas un TTL déjà distribué. Pour préparer une migration, réduisez-le à l’avance et laissez le temps à l’ancien TTL d’expirer. Les résolveurs alimentent leur cache à des moments différents. Pour enquêter, relevez le nom d’hôte, le type d’enregistrement, le résolveur interrogé, l’adresse obtenue et le TTL restant.
L’interface de résolution du système peut aussi utiliser une configuration locale. En Python, socket.getaddrinfo renvoie les informations d’adresse nécessaires à une connexion, sans TTL DNS. L’expérience ci-dessous résout localhost par cette interface : elle montre la résolution locale d’un nom, pas un échange DNS public. Le cache DNS conserve des enregistrements de noms ; le cache HTTP conserve des réponses. Recharger une page ne prouve donc pas que le cache DNS a été vidé. Modèle de cache HTTP, RFC 9111
2. Adresses, ports, écoute et connexions
Une adresse IP dirige les données vers un point du réseau ; un port aide le système d’exploitation à distinguer les services. Un serveur TCP lie un socket à une adresse et à un port, puis se met à l’écoute des nouvelles connexions. Une liaison à 127.0.0.1 accepte les connexions par la boucle locale IPv4 ; 0.0.0.0 couvre toutes les interfaces IPv4 locales. Cette dernière adresse sert de joker pour l’écoute : le client utilise une adresse réellement joignable. Documentation des sockets IPv4 sous Linux L’accès extérieur dépend aussi du routage, des pare-feu et des redirections de ports.
Une connexion TCP est identifiée par deux extrémités, chacune comprenant une adresse et un port. Imaginons un client qui se connecte de 192.0.2.20:53000 à 203.0.113.10:443. Le port 53000 appartient à cette connexion côté client ; 443 est le port du service. D’autres clients peuvent contacter le même port de destination 443, tout en restant distingués par le système. TCP fournit un flux d’octets fiable et ordonné, avec des accusés de réception et des retransmissions pour traiter les pertes. Une ouverture ordinaire passe par SYN, SYN-ACK et ACK. RFC 9293 §2.2 et §3.5
Une connexion établie rend le chemin de transport utilisable. L’accusé de réception TCP des octets ne prouve toutefois pas que l’application a analysé la requête, vérifié les permissions ou validé une transaction en base de données. Si la connexion tombe après l’envoi d’une commande, le client peut ignorer si celle-ci a été créée. Il ne devrait pas répéter automatiquement une requête non idempotente, sauf s’il sait que sa sémantique réelle permet une répétition sans effet supplémentaire ou peut établir que la première requête n’a jamais été appliquée. L’idempotence signifie que répéter la requête produit le même effet attendu qu’une seule exécution. RFC 9110 §9.2.2
Ce parcours TCP correspond à l’expérience HTTP/1.1 ci-dessous et à l’usage courant de HTTPS sur TCP. HTTP/3 utilise QUIC sur UDP avec une négociation TLS. Pour diagnostiquer HTTP/3, vérifiez le chemin UDP : un port TCP 443 joignable ne suffit pas à établir que ce transport fonctionne. RFC 9114 §3
3. TLS : vérifier le correspondant et protéger le canal
Lors d’une nouvelle connexion HTTPS utilisant des certificats, la négociation TLS établit les clés ; le serveur présente son certificat et prouve qu’il possède la clé privée correspondante. TLS 1.3 vise l’authentification, la confidentialité et l’intégrité : une fois le canal établi, seules ses extrémités peuvent lire les données, et une modification hostile est détectable. La longueur des données peut rester visible. RFC 8446 §1 et §4.4.3
Le client doit vérifier deux points distincts : la validité du certificat, notamment sa chaîne de confiance et sa période de validité (RFC 5280 §6.1), et la correspondance de son identité avec l’hôte recherché. Pour api.example.com, la correspondance se fait avec les noms DNS de l’extension subjectAltName. Le client ne doit pas remplacer l’identité attendue par un nom arbitraire fourni par le serveur. Un accès par adresse IP exige l’identifiant IP correspondant ; le certificat d’un domaine hébergé sur la même machine ne suffit pas à lui seul. RFC 9525 §6
SNI, Server Name Indication, permet au client d’indiquer l’hôte recherché pendant la négociation TLS, afin que le serveur puisse choisir un certificat. Cette étape précède le champ HTTP Host. RFC 6066 §3 Dans un test, remplacer le nom d’hôte par une adresse IP peut modifier SNI, la vérification du certificat et le routage HTTP, même si la connexion atteint la même destination. Comparez ces éléments séparément.
TLS protège le canal entre ses deux extrémités. Un proxy inverse qui termine TLS peut lire le contenu HTTP ; la connexion entre ce proxy et le serveur applicatif nécessite une évaluation distincte du chiffrement. Après vérification du certificat, l’application doit encore contrôler les identifiants de l’utilisateur et les permissions métier. Les résultats DNS et les connexions établies peuvent être réutilisés : chaque requête HTTP n’exige donc pas une nouvelle résolution, une nouvelle connexion et une négociation TLS complète.
4. HTTP : méthodes, codes d’état, en-têtes et corps
Une requête HTTP porte une méthode et une cible ; une réponse porte un code d’état. Les deux peuvent comprendre des en-têtes et un corps. Les lignes textuelles de HTTP/1.1 et les trames de HTTP/2 ou HTTP/3 expriment différemment les messages, tout en partageant cette sémantique de base. RFC 9110 §6
Ces définitions figurent dans la RFC 9110 §9.3. Considérez ensemble la méthode et le chemin : un chemin peut accepter POST et refuser GET.
Les en-têtes précisent le sens du message. Host — souvent :authority en HTTP/2 et HTTP/3 — identifie l’hôte cible et son port éventuel. Content-Type indique le type de média du corps. Content-Length compte les octets, pas les caractères, et permet de délimiter les réponses HTTP/1.1 de l’expérience. Authorization transporte les informations d’authentification. Déclarer un type JSON ne dispense pas le corps de respecter le contrat de l’API. RFC 9110 §7.2, §8.3, §8.6 et §11.6.2
Le premier chiffre du code distingue les réponses informatives 1xx, les succès 2xx, les redirections 3xx, les erreurs client 4xx et les erreurs serveur 5xx. Pour diagnostiquer un problème, lisez le code précis et le corps. RFC 9110 §15
Après réception des en-têtes, vérifiez que le corps est complet. La connexion peut tomber au milieu de la réponse, ou le JSON être impossible à analyser. Il s’agit de pannes ultérieures ; un code d’état ne remplace pas un résultat métier, ni l’inverse.
5. Proxys et localhost : situer l’environnement du client
Un proxy direct envoie des requêtes pour le compte du client. Un proxy inverse, ou passerelle, reçoit les requêtes des clients et les transmet à un serveur applicatif. RFC 9110 §3.7 Supposons que le point d’entrée public termine HTTPS et utilise HTTP vers l’application. Le parcours comporte deux connexions indépendantes :
Si le navigateur reçoit un 502 du point d’entrée, il a obtenu une réponse HTTP sur la première liaison, alors que le serveur applicatif peut être en panne. Distinguez la connexion du client au point d’entrée de celle du point d’entrée à l’application, et consultez les journaux du point d’entrée pour localiser la défaillance.
localhost désigne la boucle locale de l’environnement qui ouvre la connexion, couramment IPv4 127.0.0.1 ou IPv6 ::1. Les bibliothèques de résolution devraient traiter ce nom spécialement, normalement sans interroger le DNS public. RFC 6761 §6.3 Un navigateur sur un ordinateur portable qui ouvre localhost:8080 contacte un service de cet ordinateur, pas celui d’un VPS distant.
Dans un conteneur, identifiez aussi l’espace de noms réseau : l’ensemble des interfaces et des routes visibles par le processus. Avec un espace de noms distinct, 127.0.0.1 désigne le conteneur lui-même ; le partage de la pile réseau de l’hôte ou d’un autre conteneur modifie cette portée. Documentation réseau de Docker Un proxy dans un conteneur séparé ne peut donc pas utiliser son propre localhost pour atteindre automatiquement le conteneur applicatif. Il lui faut une adresse ou un nom de service joignable.
Pour la configuration, poursuivez avec la vue d’ensemble du VPS et son architecture et sa sécurité. Cloudflare Workers explique comment un environnement géré reçoit les requêtes et renvoie les réponses. MCP décrit les messages d’outils et l’autorisation lors de l’utilisation de son transport HTTP.
6. Expérience locale : une connexion fonctionnelle peut renvoyer 404
Enregistrez le code ci-dessous dans request_trace.py, puis exécutez python3 request_trace.py. Il utilise uniquement la bibliothèque standard de Python, démarre un service HTTP temporaire sur la boucle locale IPv4, envoie deux requêtes GET, puis tente de joindre un port lié à un socket mais sans écoute. Le port serveur est fixé à 0 pour laisser le système choisir un port disponible ; les numéros peuvent changer d’une exécution à l’autre.
Le serveur utilise http.server, et le client http.client. Cette expérience emploie HTTP en clair pour montrer la résolution locale, TCP et HTTP. Les contrôles du certificat et la négociation TLS correspondent au parcours HTTPS décrit plus haut.
Ce gestionnaire implémente uniquement GET : /health renvoie 200, et les autres chemins renvoient 404. BaseHTTPRequestHandler.path comprend la chaîne de requête ; urlsplit sépare donc le chemin des paramètres. Cette expérience ignore les paramètres de requête : /health?full=1 renvoie le même résultat de contrôle de santé.
import socket
from http.client import HTTPConnection
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
from urllib.parse import urlsplit
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if urlsplit(self.path).path == "/health":
status, body = 200, b'{"ok":true}\n'
else:
status, body = 404, b'{"error":"not_found"}\n'
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
pass
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
port = server.server_address[1]
worker = Thread(target=server.serve_forever, daemon=True)
worker.start()
conn = HTTPConnection("localhost", port, timeout=2)
try:
results = socket.getaddrinfo(
"localhost", port, socket.AF_INET, socket.SOCK_STREAM
)
print("resolve localhost (IPv4):", sorted({r[4][0] for r in results}))
print(f"listen: 127.0.0.1:{port}")
conn.connect()
print(f"TCP: {conn.sock.getsockname()} -> {conn.sock.getpeername()}")
for path in ("/health", "/missing"):
conn.request("GET", path, headers={"Host": f"localhost:{port}"})
response = conn.getresponse()
body = response.read()
print(f"GET {path} -> {response.status}; "
f"type={response.getheader('Content-Type')}; "
f"length={response.getheader('Content-Length')}; read={len(body)}")
print(body.decode("utf-8"), end="")
finally:
conn.close()
server.shutdown()
server.server_close()
worker.join()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as unused:
unused.bind(("127.0.0.1", 0))
try:
with socket.create_connection(unused.getsockname(), timeout=2):
print("unexpected connection")
except (ConnectionRefusedError, TimeoutError) as error:
print("bound port without listen:", type(error).__name__)
Sortie d’une exécution avec Python 3.13.15 :
resolve localhost (IPv4): ['127.0.0.1']
listen: 127.0.0.1:64242
TCP: ('127.0.0.1', 64244) -> ('127.0.0.1', 64242)
GET /health -> 200; type=application/json; length=12; read=12
{"ok":true}
GET /missing -> 404; type=application/json; length=22; read=22
{"error":"not_found"}
bound port without listen: TimeoutError
64242 est le port d’écoute du serveur ; 64244 est le port source du client pour cette connexion. Les deux requêtes réutilisent cette connexion et n’envoient aucun corps. Les corps des réponses contiennent respectivement 12 et 22 octets UTF-8, saut de ligne final compris. length est la taille déclarée par le serveur ; read la taille réellement reçue. Le 404 de /missing montre que l’échange HTTP fonctionne : recherchez le problème dans le chemin de la ressource plutôt que dans l’établissement de la connexion.
La dernière tentative a expiré lors de cette exécution ; un autre système peut la refuser immédiatement. Aucun de ces résultats ne comporte de code HTTP. Ici, l’expérience établit que le port n’est pas à l’écoute, ce qui permet d’en connaître la cause. Sur un réseau réel, un délai dépassé ne suffit pas à distinguer un problème d’écoute d’un problème de pare-feu ou de routage.
7. Diagnostiquer à partir de la dernière couche fonctionnelle
Relevez l’environnement qui lance la requête, l’URL, le correspondant réellement joint, la dernière étape réussie et le message d’erreur exact. Vérifiez chaque étape depuis le même client, sans réunir artificiellement les observations du portable, du conteneur et du serveur en un seul parcours.
Un contrôle de santé qui renvoie 200 établit que le chemin testé fonctionne. Si la tâche consiste à créer une commande, vérifiez aussi l’authentification de cette API, ses entrées et le résultat enregistré. Chaque étape doit répondre à une question précise avant de passer aux couches suivantes.