Aller au contenu

Déboguer des requêtes HTTP avec curl : sortie verbeuse, chronométrage et en-têtes

Un guide pratique pour debug HTTP with curl : lire la sortie verbeuse -v, décomposer la latence en DNS, connexion, TLS et TTFB avec -w, inspecter les en-têtes et les redirections, forcer une résolution et diagnostiquer les pannes courantes.

Quand une requête est lente, échoue ou répond de travers, curl -v vous montre toute la conversation — les octets exacts envoyés, les octets exacts reçus, et les notes de connexion et de TLS entre les deux — pendant que -w découpe la latence en phases pour que vous puissiez dire où le temps est passé plutôt que juste qu’elle était lente. Entre ces deux drapeaux, vous pouvez trancher presque n’importe quel débat « est-ce nous ou est-ce eux » sans ouvrir un navigateur.

Ce que vous allez accomplir

  • Voir la requête et la réponse complètes, telles que curl les envoie et les reçoit, en-têtes compris.
  • Décomposer l’aller-retour en DNS, connexion TCP, poignée de main TLS et temps jusqu’au premier octet, et nommer la phase lente.
  • Inspecter les en-têtes, les codes de statut et chaque saut d’une chaîne de redirections.
  • Reproduire une panne en une ligne et isoler s’il s’agit d’un problème d’origine, de DNS ou de TLS.

Prérequis

  • curl 7.x ou 8.x. Vérifiez avec curl --version ; toute version des dernières années fait l’affaire. Les variables de chronométrage %{...} utilisées ici sont anciennes et sans risque — elles sont livrées depuis bien plus d’une décennie. Quelques drapeaux sont plus récents : --fail-with-body demande 7.76+, et --json demande 7.82+, alors ce guide s’en tient à --data et -H par souci de portabilité.
  • Un shell. Tout ici est une seule commande que vous pouvez coller et lire.
  • Une URL à tester. Un point d’accès public comme https://example.com suffit pour suivre ; remplacez-la par votre propre origine quand vous diagnostiquez pour de vrai.
  • Être honnête sur ce que curl vous montre : la vue transport, pas la vue navigateur. curl n’exécute aucun JavaScript, ne partage rien avec le cache ni les témoins de votre navigateur, et ne suit aucun rafraîchissement <meta>. Il vous dit ce que le serveur a réellement envoyé — souvent exactement l’écart que vous cherchez quand « ça marche dans curl mais pas dans le navigateur », ou l’inverse.

Comment ça marche

Chaque requête HTTP(S) que curl émet traverse la même séquence figée, et curl peut chronométrer chaque frontière. Il résout le nom d’hôte en adresse (DNS), ouvre une connexion TCP vers celle-ci (connexion), négocie TLS par-dessus cette connexion si le schéma est https (curl appelle ça appconnect), envoie la requête, attend pendant que le serveur travaille, reçoit le premier octet de la réponse (starttransfer), et finit enfin de lire le corps (total).

Une cascade de chronométrage horizontale montrant les phases que curl mesure — le DNS avec time_namelookup, la connexion TCP avec time_connect, la poignée de main TLS avec time_appconnect, le traitement serveur, le premier octet avec time_starttransfer, et la requête entière avec time_total — chacune étiquetée avec sa variable curl -w cumulative mesurée depuis le début

Deux choses dans cette image comptent pour lire la sortie. D’abord, avec -v, curl annote la conversation en direct : les lignes qui commencent par * sont ses propres notes sur la connexion et le TLS, les lignes qui commencent par > sont les octets que curl a envoyés, et celles qui commencent par < sont les octets qu’il a reçus. Ensuite, chaque variable de chronométrage -w est cumulative depuis le début de la requête, pas la durée d’une seule phase. time_appconnect est la lecture de l’horloge au moment où le TLS s’est terminé — donc la poignée de main TLS à elle seule, c’est time_appconnect moins time_connect. Gardez cette soustraction en tête et les chiffres cessent d’être déroutants.

Étape par étape

1. Observez toute la conversation avec -v. C’est la première chose à lancer sur n’importe quelle URL qui fait des siennes. Lisez les préfixes : * pour les notes de connexion et de TLS, > pour ce que vous avez envoyé, < pour ce qui est revenu.

curl -v https://example.com
*   Trying 93.184.216.34:443...
* Connected to example.com (93.184.216.34) port 443
* ALPN: server accepted h2
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* Server certificate:
*  subject: CN=example.com
*  issuer: C=US; O=DigiCert Inc; CN=DigiCert TLS RSA SHA256 2020 CA1
> GET / HTTP/2
> Host: example.com
> user-agent: curl/8.6.0
> accept: */*
>
< HTTP/2 200
< content-type: text/html; charset=UTF-8
< content-length: 1256
<

Le signal de succès, c’est la ligne < HTTP/2 200 : une réponse est revenue avec un statut. Le bloc * au-dessus vous donne le protocole négocié (h2) et la version de TLS, et confirme quel certificat le serveur a présenté — la façon la plus rapide d’attraper un certificat erroné ou expiré.

2. Demandez seulement les en-têtes. Quand ce sont le statut et les en-têtes qui vous intéressent, pas le corps, -I envoie une requête HEAD et n’imprime que les en-têtes de réponse. Utilisez plutôt -i quand vous voulez les en-têtes et le corps d’un GET ordinaire.

curl -I https://example.com
HTTP/2 200
content-type: text/html; charset=UTF-8
content-length: 1256
cache-control: max-age=604800
date: Sun, 26 Jul 2026 14:02:11 GMT

Une réserve : certains serveurs traitent HEAD autrement que GET (quelques-uns le rejettent carrément, ou un CDN rapporte un statut de cache différent). Si un HEAD semble louche, confirmez avec curl -i avant de blâmer le serveur.

3. Décomposez la latence en phases avec -w. Le drapeau -w (write-out) imprime des variables après la fin du transfert. Dirigez le corps vers /dev/null avec -o et faites taire la jauge de progression avec -s pour que seule votre ligne de chronométrage s’affiche. Cette chaîne de format vaut la peine d’être gardée dans un alias de shell :

curl -w 'dns:%{time_namelookup}s connect:%{time_connect}s tls:%{time_appconnect}s ttfb:%{time_starttransfer}s total:%{time_total}s\n' -o /dev/null -s https://example.com
dns:0.012s connect:0.045s tls:0.128s ttfb:0.310s total:0.352s

Lisez-la comme des lectures d’horloge cumulatives, puis soustrayez pour trouver chaque phase. Ici le DNS a pris 12 ms, la poignée de main TCP a ajouté 33 ms (0.045 − 0.012), le TLS a ajouté 83 ms (0.128 − 0.045), le serveur a réfléchi 182 ms avant le premier octet (0.310 − 0.128), et le corps a pris les 42 derniers ms. Le gros chiffre vous dit quelle équipe appeler.

4. Suivez les redirections et voyez chaque saut. Par défaut curl vous montre une réponse et s’arrête. Ajoutez -L pour suivre les en-têtes Location, et combinez avec -v (ou -I -L) pour observer chaque saut se résoudre.

curl -I -L http://example.com
HTTP/1.1 301 Moved Permanently
location: https://example.com/

HTTP/2 200
content-type: text/html; charset=UTF-8

Chaque bloc est un saut. C’est ainsi que vous confirmez qu’une montée de http vers https se déclenche exactement une fois, ou que vous attrapez une chaîne qui rebondit sur trois noms d’hôte avant d’atterrir — une latence que vous ne verriez jamais autrement.

5. Forcez la résolution pour atteindre une origine précise. Derrière un CDN ou un répartiteur de charge, le DNS vous pointe vers la périphérie, pas vers la machine que vous voulez tester. --resolve host:port:address remplace le DNS pour ce seul nom et ce seul port, si bien que l’URL, l’en-tête Host et le SNI TLS restent corrects pendant que la connexion va où vous la dirigez.

curl -v --resolve example.com:443:203.0.113.10 https://example.com
* Added example.com:443:203.0.113.10 to DNS cache
*   Trying 203.0.113.10:443...
* Connected to example.com (203.0.113.10) port 443

La ligne Connected to example.com (203.0.113.10) est la preuve : le nom d’hôte est inchangé mais l’adresse est celle que vous avez choisie. C’est ainsi que vous testez un serveur d’arrière-plan derrière un tourniquet, ou une nouvelle origine avant de basculer le DNS vers elle.

6. Rendez les pannes scriptables. Par défaut curl imprime le corps d’erreur du serveur et sort quand même avec le code 0 sur un HTTP 404 ou 500 — un piège dans les scripts. --fail fait sortir curl avec un code non nul sur tout 4xx/5xx sans rien imprimer ; --fail-with-body fait pareil mais garde le corps d’erreur pour que vous puissiez le journaliser. Associez-le à -o /dev/null -s et -w '%{http_code}' pour une vérification nette.

curl --fail-with-body -s -o /dev/null -w '%{http_code}\n' https://example.com/does-not-exist
404

La commande sort avec un code non nul (vérifiez avec echo $?), donc elle s’insère directement dans un contrôle de santé ou une étape d’intégration continue. Un 000 imprimé au lieu d’un code signifie qu’aucune réponse HTTP n’est arrivée du tout — connexion refusée, panne de DNS ou délai dépassé — ce qui est une classe de problème différente de n’importe quel vrai statut.

7. Envoyez un POST JSON et lisez la réponse. -X POST fixe la méthode, -H fixe un en-tête, et --data fournit le corps. Passez la réponse dans jq pour la mettre en forme et l’interroger.

curl -s -X POST -H 'Content-Type: application/json' --data '{"name":"harbor","region":"ca"}' https://example.com/api/echo | jq .
{
  "name": "harbor",
  "region": "ca"
}

Si jq échoue sur l’entrée, le serveur n’a pas renvoyé de JSON — retirez le tube et lancez le curl -i brut pour voir le vrai Content-Type et le corps, qui est souvent une page d’erreur HTML déguisée.

Valider que ça fonctionne

Trois vérifications rapides confirment que vous lisez bien les outils. Dans la sortie -v, les lignes * devraient nommer un vrai protocole et une vraie version de TLS — ALPN: server accepted h2 et SSL connection using TLSv1.3 — preuve que la poignée de main s’est vraiment terminée plutôt que curl ne devine. Dans la ligne -w, les sommes devraient tenir la route : en HTTPS, time_appconnect est toujours plus grand que time_connect parce que le TLS arrive après la poignée de main TCP ; en HTTP simple, time_appconnect affiche 0.000000 parce qu’il n’y a aucune phase TLS — une façon rapide de confirmer que vous avez bel et bien visé http et non https. Et quand vous utilisez --resolve, la ligne Connected to <host> (<address>) doit montrer l’adresse que vous avez forcée, pas celle que le DNS aurait renvoyée ; si elle montre l’ancienne adresse, votre host:port ne correspondait pas exactement à l’URL.

Dépannage

  • SSL certificate problem. curl a refusé le certificat — autosigné, expiré, ou émis pour un autre nom d’hôte. -k (--insecure) saute la vérification et convient pour un test local vite fait, mais jamais dans un script ni contre quoi que ce soit de réel, parce que ça désactive justement le contrôle qui donne un sens à une connexion TLS. Les bons correctifs : si le certificat est valide mais que vous avez atteint la mauvaise machine, utilisez --resolve pour viser la bonne origine ; s’il s’agit d’une autorité de certification privée, faites-lui confiance explicitement avec --cacert /path/to/ca.pem plutôt que de désactiver la vérification en bloc.
  • Could not resolve host. Le DNS a échoué avant toute tentative de connexion, donc ce n’est pas un problème d’origine. Confirmez que le nom se résout tout court (getent hosts example.com ou dig example.com), vérifiez /etc/resolv.conf, et si l’enregistrement n’est simplement pas encore publié, utilisez --resolve pour pointer curl vers l’adresse voulue et continuer à tester.
  • Connection refused par rapport à un blocage. Ce sont deux pannes différentes. Connection refused est immédiat et signifie que l’adresse est joignable mais que rien n’écoute sur ce port — le mauvais port, ou un service arrêté. Une requête qui reste bloquée jusqu’au délai dépassé signifie que les paquets partent dans le silence — un pare-feu ou un groupe de sécurité qui les jette, ou carrément la mauvaise adresse. Le refus pointe vers le service ; un délai dépassé pointe vers le chemin réseau.
  • Une boucle de redirection. Si -L tourne en rond et finit par s’arrêter avec Maximum (50) redirects followed, le site rebondit entre des emplacements — souvent une règle http vers https qui retire aussi le schéma, ou un témoin que la cible de redirection réclame sans cesse. Plafonnez-le plus bas pour voir la boucle clairement avec --max-redirs 5, puis lisez les en-têtes Location avec -v pour trouver les deux URL entre lesquelles ça fait la navette.
  • Le corps ressemble à du charabia. Une réponse pleine de bruit binaire signifie d’ordinaire qu’elle est compressée en gzip ou en brotli et que vous l’avez demandée brute. Ajoutez --compressed pour que curl annonce les encodages qu’il sait gérer et décode la réponse. Si c’est toujours illisible après ça, vérifiez le Content-Type — vous regardez peut-être une image ou un protobuf, pas du texte.

Quand ce n’est pas la bonne approche

curl montre le transport, ce qui cesse de suffire dès que le problème vit au-dessus. Si une page ne s’affiche mal que dans un navigateur — mise en page, un script qui échoue, une interaction de cache ou de témoins — curl ne la reproduira pas, parce qu’il n’exécute aucun JavaScript et ne garde aucun état de navigateur ; les onglets Réseau et Console des outils de développement du navigateur sont les bons outils là. Si la question est sous le HTTP — des paquets qui n’arrivent jamais, des retransmissions, une poignée de main qui cale au niveau TCP — il vous faut une capture de paquets ; commencez par capturer et filtrer le trafic avec tcpdump. Et curl mesure une requête à la fois, alors c’est le mauvais instrument pour les tests de charge : pour savoir comment un point d’accès se comporte sous concurrence, prenez un outil de charge dédié plutôt qu’une boucle de shell autour de curl. La référence complète des drapeaux vit dans le manuel de curl.

Si c’est le genre de travail que vous préféreriez ne plus jamais porter, c’est le quotidien d’Opérations Cloud — réservez une revue d’infrastructure.