Tester un endpoint d'API : DNS, TLS, en-têtes et CORS

Point clé

« L'API est en panne » n'est presque jamais un seul problème. Testez six couches (DNS, port 443, TLS, statut HTTP, CORS, charge utile) et arrêtez-vous à la première qui échoue. Les coupables habituels : un enregistrement DNS jamais propagé, un certificat expiré dans la nuit, ou un en-tête Access-Control-Allow-Origin absent qui casse les appels du navigateur alors que curl fonctionne très bien.

Vérifiez maintenant

Schéma des six couches pour tester un endpoint d'API public : résolution DNS, port 443, certificat TLS, en-têtes HTTP, CORS et charge utile JSON
Schéma des six couches pour tester un endpoint d'API public : résolution DNS, port 443, certificat TLS, en-têtes HTTP, CORS et charge utile JSON
Commencer la lecture
  1. Pourquoi « l'API est en panne » n'est jamais un seul problème
  2. Choisir un vrai endpoint que vous pouvez solliciter à volonté
  3. Couche 1 : le nom d'hôte se résout-il ?
  4. Couche 2 : le port 443 est-il ouvert ?
  5. Couche 3 : le certificat TLS est-il valide ?
  6. Couche 4 : lire les en-têtes de réponse
  7. À quoi ressemble une réponse JSON saine
  8. Couche 5 : l'en-tête qui ne casse que les appels du navigateur
  9. Le piège du preflight : ça marche jusqu'à l'ajout d'un en-tête
  10. Couche 6 : la charge utile a-t-elle la forme attendue ?
  11. Symptôme → couche → outil
  12. Questions fréquentes

Pourquoi « l'API est en panne » n'est jamais un seul problème

Quand un appel d'API échoue, l'erreur affichée par votre client est généralement produite plusieurs couches au-dessus de ce qui est réellement cassé. Un navigateur qui affiche Failed to fetch ne peut pas dire si le nom d'hôte n'a pas été résolu, si la connexion TCP a été refusée, si la poignée de main TLS a été rejetée, ou si le serveur a renvoyé un 200 parfaitement sain que le navigateur a ensuite jeté faute d'en-tête CORS. Les quatre cas donnent le même message.

La solution : arrêter de deviner et parcourir la pile dans l'ordre. Chaque couche n'a de sens que si celle du dessous a réussi ; la première couche qui échoue est donc votre vrai bug, et tout ce qui suit n'est que du bruit.

Six couches méritent d'être vérifiées, et l'ensemble prend environ deux minutes :

  • DNS : le nom d'hôte se résout-il en une IP ?
  • Port 443 : quelque chose écoute-t-il, ou la connexion est-elle refusée ou filtrée ?
  • TLS : le certificat est-il valide, non expiré, et servi avec une chaîne complète ?
  • Statut HTTP : que renvoie réellement le serveur : 200, 401, 429, 503 ?
  • CORS : la réponse porte-t-elle les en-têtes dont un navigateur a besoin pour vous remettre le corps ?
  • Charge utile : le JSON a-t-il la forme attendue par votre code ?

Vérifiez dès maintenant le statut et les en-têtes d'un endpoint

Choisir un vrai endpoint que vous pouvez solliciter à volonté

Pour vous entraîner, il vous faut un endpoint en ligne qui renvoie de vraies données, n'exige aucune clé d'API et ne vous limitera pas si vous lancez vingt fois la même requête. Cela exclut la plupart des candidats évidents : presque toutes les API publiques courantes exigent désormais un formulaire d'inscription et un jeton, si bien qu'une requête en échec pourrait toujours venir de vos identifiants plutôt que de la couche testée.

Les exemples ci-dessous utilisent l'API gratuite d'horaires de prière de Waqt Azan (مواقيت الصلاة), un service arabe d'horaires de prière couvrant plus de 60 000 lieux dans plus de 100 pays. Il convient bien comme endpoint d'apprentissage pour trois raisons : ni clé ni en-tête d'authentification, du JSON structuré plutôt que du HTML, et une politique CORS permissive. Les commandes curl s'exécutent donc telles quelles, et l'exemple fetch() fonctionne depuis la console d'une page sans Content-Security-Policy stricte.

L'endpoint prend un slug de pays et de ville directement dans le chemin :

bash
# Prayer times for a city — no API key, no headers required
curl -s https://waqtazan.com/api/v1/timings/saudi-arabia/riyadh

# The same data as a subscribable calendar feed
curl -s https://waqtazan.com/api/v1/ical/saudi-arabia/riyadh.ics

Remplacez-le par l'hôte que vous déboguez réellement : les six couches sont identiques, qu'il s'agisse d'une API publique, de votre propre serveur de préproduction ou d'un récepteur de webhook tiers.

Couche 1 : le nom d'hôte se résout-il ?

Le DNS est un coupable fréquent. Un enregistrement a été modifié et ne s'est pas propagé, un wildcard a été supprimé, ou l'API vit sur un sous-domaine qui n'a jamais été créé.

Résolvez le nom d'hôte avant toute autre chose. Si cette étape ne renvoie rien, redémarrer votre application n'y changera rien : votre client n'arrive même pas à ouvrir un socket.

bash
dig +short waqtazan.com A
nslookup waqtazan.com

# Compare what different resolvers see — catches mid-propagation records
dig +short waqtazan.com A @1.1.1.1
dig +short waqtazan.com A @8.8.8.8

Si deux résolveurs publics ne sont pas d'accord juste après une modification, l'enregistrement est encore en cours de propagation et l'API semblera cassée par intermittence selon le résolveur utilisé par chaque client (un CDN à routage géographique peut tout le temps renvoyer une adresse différente à chaque résolveur : comparez donc avec l'enregistrement que vous avez défini). Lancez une recherche DNS pour voir l'ensemble des enregistrements, ou vérifiez la propagation sur les résolveurs mondiaux avec le vérificateur de propagation DNS juste après la modification.

Une réponse vide ici correspond directement à ERR_NAME_NOT_RESOLVED dans Chrome et à getaddrinfo ENOTFOUND dans Node : ERR_NAME_NOT_RESOLVED donne la liste complète des corrections.

Un point à ne pas surveiller : beaucoup d'API se trouvent derrière un CDN, donc l'enregistrement A pointe vers un point de présence du CDN plutôt que vers le serveur d'origine. C'est normal. Ce qui compte, c'est d'obtenir une réponse, pas de savoir quelle IP.

Couche 2 : le port 443 est-il ouvert ?

Le nom d'hôte se résout, donc il y a une adresse. Reste à savoir si quelque chose y écoute. C'est ici que l'on sépare trois pannes très différentes que les clients rapportent pourtant de la même façon :

  • Ouvert : la poignée de main TCP aboutit. Passez à la couche suivante.
  • Refusé : l'hôte a répondu par un RST immédiat. Il y a bien quelque chose, mais rien n'est lié à ce port : en général le service a planté, ou n'écoute que sur localhost.
  • Filtré : la connexion reste en attente jusqu'au délai d'expiration. Un pare-feu ou un groupe de sécurité abandonne les paquets en silence, d'où une requête qui patiente une minute ou plus avant d'échouer (75 secondes sur macOS).
bash
# Time-boxed TCP check: -w limits the connect on Linux; on macOS use -G 5 instead
nc -vz -w 5 waqtazan.com 443

# Or with curl: --connect-timeout caps the connect; 000 means no response came back
curl -s -o /dev/null -w '%{http_code}\n' --connect-timeout 5 https://waqtazan.com/

Une connexion refusée et une connexion filtrée demandent des corrections totalement différentes ; mieux vaut donc savoir laquelle vous avez. Le vérificateur de ports les distingue et teste depuis l'extérieur de votre réseau, ce qui compte : un port ouvert depuis le VPN de votre bureau peut être bloqué par un pare-feu côté Internet public.

Si curl affiche 000 comme code de statut, aucune réponse HTTP n'est revenue. En général la requête a échoué dans les couches 1 à 3 (pas de réponse DNS, port refusé ou filtré, poignée de main TLS en échec), mais un serveur qui accepte la connexion puis la ferme, ou ne répond jamais, donne aussi 000 ; ajoutez -v pour voir la dernière étape réussie. ERR_CONNECTION_REFUSED détaille le cas du refus.

Couche 3 : le certificat TLS est-il valide ?

Le port 443 est ouvert, mais un port ouvert ne signifie pas un endpoint HTTPS fonctionnel. Les certificats sont une cause classique de panne nocturne : ils expirent à date fixe et l'automatisation du renouvellement échoue sans bruit.

Trois conditions doivent être réunies : le certificat n'est pas expiré, son nom correspond au nom d'hôte demandé, et le serveur envoie la chaîne intermédiaire complète. La troisième est sournoise : une chaîne incomplète passe souvent dans les navigateurs, car Chrome télécharge lui-même l'intermédiaire manquant et Firefox les précharge, alors que Node, Python, Java et les builds de curl basés sur OpenSSL échouent (le curl livré avec macOS utilise le vérificateur d'Apple et peut passer). C'est l'origine du classique rapport de bug « ça marche dans mon navigateur mais pas en production ».

bash
# Expiry dates and subject
echo | openssl s_client -connect waqtazan.com:443 -servername waqtazan.com 2>/dev/null \
  | openssl x509 -noout -subject -dates

# Full chain as presented by the server
openssl s_client -connect waqtazan.com:443 -servername waqtazan.com -showcerts < /dev/null

Comptez les certificats dans la sortie de -showcerts. Un seul certificat signifie généralement un intermédiaire manquant. Le vérificateur de certificat SSL indique l'expiration, la complétude de la chaîne et la correspondance du nom d'hôte en une seule passe, et ce qu'est une chaîne de certificats SSL explique pourquoi l'intermédiaire est si important.

Gardez -servername : OpenSSL 1.1.1 et suivants renseignent le SNI à partir du nom d'hôte de -connect, mais le openssl LibreSSL livré avec macOS ne le fait pas, et aucune version ne le fait quand vous vous connectez à une adresse IP. Sans SNI, un hébergement mutualisé vous renvoie son certificat par défaut ou, comme Cloudflare, interrompt la poignée de main, et vous traquez une erreur de nom qui n'existe pas.

Couche 4 : lire les en-têtes de réponse

TLS est bon : vous avez enfin une vraie conversation HTTP. Lisez maintenant ce que le serveur a réellement dit, plutôt que ce que votre client en a résumé. N'affichez que les en-têtes : le corps est quand même téléchargé puis jeté, c'est donc le même GET que celui de votre client.

bash
curl -s -D - -o /dev/null https://waqtazan.com/api/v1/timings/egypt/cairo

Lisez d'abord la ligne de statut, puis content-type, puis le reste. Ces deux champs vous disent si la requête a réussi et si le corps est du JSON avant que vous ne lisiez quoi que ce soit d'autre.

À quoi ressemble une réponse JSON saine

La commande ci-dessus, lancée le 27 octobre 2026 sur l'endpoint de test, a renvoyé ces en-têtes, entre autres :

En-têteValeurCe que cela vous apprend
HTTP/2200La requête a réussi, et le serveur parle HTTP/2
content-typeapplication/jsonVous recevrez du JSON, pas une page d'erreur HTML
content-length567Une vraie charge utile, pas un corps vide
access-control-allow-origin*Les navigateurs ont le droit de lire cette réponse
servercloudflareUn CDN se trouve devant l'origine
alt-svch3=":443"HTTP/3 est annoncé pour les requêtes suivantes

Couche 5 : l'en-tête qui ne casse que les appels du navigateur

C'est ici que les heures s'évaporent, car c'est la couche où curl et le navigateur divergent vraiment. CORS est appliqué par le navigateur, pas par le serveur. Si Access-Control-Allow-Origin est absent, le serveur renvoie quand même un 200 complet et correct : le navigateur refuse simplement de remettre le corps à votre JavaScript.

Le symptôme est donc : curl fonctionne parfaitement, Postman fonctionne parfaitement, et la console du navigateur indique que la requête a été bloquée par la politique CORS. Rien ne cloche côté serveur. Il manque simplement un en-tête à la réponse.

L'endpoint de test renvoie access-control-allow-origin: *, c'est pourquoi un simple fetch() depuis n'importe quelle origine fonctionne :

javascript
// Run it in the console of a tab on a site without a strict CSP, such as example.com
// (this page's own CSP blocks it). It works there because ACAO is set to *
const r = await fetch('https://waqtazan.com/api/v1/timings/egypt/cairo')
console.log(await r.json())

Le piège du preflight : ça marche jusqu'à l'ajout d'un en-tête

Il existe un second piège, que cet endpoint illustre bien. Une requête simple (un GET sans en-tête personnalisé) réussit. Mais dès que vous ajoutez un en-tête personnalisé, ou faites un POST avec Content-Type: application/json, le navigateur envoie d'abord un preflight OPTIONS, et la réponse au preflight doit porter ses propres en-têtes CORS (MDN).

Testez le preflight séparément, car c'est une autre réponse, issue d'un autre chemin de code :

bash
curl -s -D - -o /dev/null -X OPTIONS \
  -H 'Origin: https://example.com' \
  -H 'Access-Control-Request-Method: GET' \
  https://waqtazan.com/api/v1/timings/egypt/cairo

Au 27 octobre 2026, ce preflight renvoie un 200 avec content-type: text/html et aucun en-tête access-control-allow-origin, de sorte qu'une requête avec preflight depuis un navigateur serait bloquée, alors même que le simple GET fonctionne.

C'est exactement le piège : votre intégration fonctionne en développement, vous ajoutez un en-tête Authorization ou X-Request-Id, et elle casse sans aucun changement côté serveur. Si vous n'avez besoin que de données publiques, gardez la requête simple et elle continuera de fonctionner.

Couche 6 : la charge utile a-t-elle la forme attendue ?

La dernière couche est celle qui intéresse vraiment votre code. Un 200 avec du JSON valide peut quand même vous casser si un champ a été renommé, si un type est passé de nombre à chaîne, ou si un horodatage arrive dans un format que vous ne gérez pas.

Regardez la vraie réponse avant d'écrire le parseur. Cet endpoint renvoie à la fois des heures formatées pour l'affichage et des horodatages ISO, un schéma à copier : les chaînes localisées sont pour les humains, les valeurs ISO pour le code :

json
{
  "city": { "slug": "riyadh", "name_ar": "الرياض", "name_en": "Riyadh" },
  "country": "saudi-arabia",
  "date": "2026-10-26",
  "times":    { "fajr": "4:38 ص", "dhuhr": "11:37 ص", "maghrib": "5:17 م" },
  "timesISO": { "fajr": "2026-10-26T01:38:00.000Z", "dhuhr": "2026-10-26T08:37:00.000Z" },
  "method": "umm_al_qura"
}

Parsez toujours le champ lisible par machine. Ici, c'est timesISO, jamais times : les chaînes d'affichage sont localisées et changeront de format dès que la locale change.

Passez la réponse dans jq pour inspecter rapidement la structure : curl -s <url> | jq keys liste les champs de premier niveau, et jq '.times' descend dans un champ sans faire défiler le corps brut.

Symptôme → couche → outil

Utilisez ce tableau pour aller directement à la bonne couche, au lieu de parcourir les six à chaque fois :

Ce que vous voyezCouche en échecÀ vérifier avec
ERR_NAME_NOT_RESOLVED, ENOTFOUND1 — DNSRecherche DNS
ECONNREFUSED, échec immédiat2 — PortVérificateur de ports
La requête se bloque, puis expire2 — Port filtréVérificateur de ports
CERT_HAS_EXPIRED, UNABLE_TO_VERIFY_LEAF_SIGNATURE3 — TLSVérificateur SSL
Fonctionne dans le navigateur, échoue avec curl ou Node3 — Chaîne incomplèteVérificateur SSL
401, 429, 5034 — Statut HTTPVérificateur d'en-têtes HTTP
Fonctionne avec curl, bloqué dans le navigateur5 — CORSVérificateur d'en-têtes HTTP
Unexpected token '<', "<!DOCTYPE "... is not valid JSON4 — Page d'erreur HTMLVérificateur d'en-têtes HTTP
Champs manquants ou de mauvais type6 — Charge utilecurl … | jq

La latence se situe un peu en dehors de cette échelle. Si toutes les couches passent mais que les réponses sont lentes, mesurez l'aller-retour vers l'hôte avec ping et vérifiez si la résolution de noms elle-même est le goulot d'étranglement avec un test de vitesse DNS.

Vérifiez les vrais en-têtes de réponse de n'importe quel endpoint

Voyez le code de statut, le content-type et les en-têtes CORS exacts renvoyés par une API, demandés côté serveur : aucun cache de navigateur ni extension ne peut fausser le résultat. Gratuit, sans inscription.

Sources

  1. Cross-Origin Resource Sharing (CORS), MDN
  2. Fetch Standard: CORS-safelisted request headers, WHATWG

Questions fréquentes

Comment tester un endpoint d'API sans Postman ?

curl couvre tout ce que fait Postman pour des tests de base. Utilisez curl -s -D - -o /dev/null <url> pour n'afficher que les en-têtes de réponse, curl -s <url> | jq pour inspecter le corps JSON, et curl -s -o /dev/null -w '%{http_code}' pour n'afficher que le code de statut. Pour un contrôle rapide sans terminal, un vérificateur d'en-têtes HTTP en ligne renvoie les mêmes informations depuis un navigateur.

Pourquoi mon API fonctionne avec curl mais échoue dans le navigateur ?

Presque toujours à cause de CORS. Le serveur renvoie une réponse valide dans les deux cas, mais les navigateurs refusent d'exposer le corps à JavaScript sans un en-tête Access-Control-Allow-Origin autorisant votre origine. curl ignore totalement CORS, car CORS est une politique de sécurité du navigateur, pas du serveur. Si l'en-tête est absent, c'est au serveur de l'envoyer : il n'existe aucune solution côté client.

Que signifie le code de statut 000 renvoyé par curl ?

Cela signifie qu'aucune réponse HTTP n'a jamais été reçue, donc qu'il n'y a pas de code de statut à afficher. L'échec s'est généralement produit avant HTTP : le DNS n'a pas résolu, la connexion TCP a été refusée ou filtrée, ou la poignée de main TLS a échoué. Un serveur qui accepte la connexion puis la ferme ou ne répond jamais donne aussi 000 ; relancez donc la commande avec -v pour voir la dernière étape qui a fonctionné.

Comment savoir si une API est en panne pour tout le monde ou seulement pour moi ?

Testez depuis l'extérieur de votre réseau. Un vérificateur de ports et une recherche DNS exécutés côté serveur éliminent de l'équation votre résolveur local, votre VPN, le pare-feu de l'entreprise et le cache du navigateur. Si l'endpoint répond depuis un vérificateur externe mais pas depuis votre machine, le problème est local : typiquement un cache DNS ou une règle de pare-feu.

Pourquoi une API renvoie 200 alors que mon parseur JSON lève une erreur ?

Vous recevez très probablement du HTML avec un statut 200. Les portails captifs, les proxys et certaines pages d'erreur renvoient un code de succès avec un corps HTML. Vérifiez l'en-tête content-type : s'il indique text/html plutôt que application/json, le corps est une page web et l'erreur de parsing est un symptôme, pas la cause.

Qu'est-ce qu'un preflight CORS et quand se produit-il ?

Un preflight est une requête OPTIONS automatique que le navigateur envoie avant la vraie requête chaque fois que celle-ci n'est pas « simple » : elle utilise une méthode autre que GET, HEAD ou POST, définit des en-têtes personnalisés comme Authorization, ou envoie un Content-Type autre que les données de formulaire ou le texte brut (application/json compte). La réponse au preflight doit elle-même inclure les en-têtes CORS. C'est pourquoi une intégration peut fonctionner sans problème jusqu'à l'ajout d'un en-tête d'authentification, puis casser sans changement côté serveur.

Dois-je tester le DNS de l'API avec mon propre résolveur ou un résolveur public ?

Les deux, en comparant les résultats. Le résolveur de votre FAI peut servir un enregistrement obsolète en cache bien après la modification de l'enregistrement faisant autorité. Interroger 1.1.1.1 et 8.8.8.8 en plus de votre résolveur par défaut montre si vous êtes face à un délai de propagation ou à un enregistrement réellement manquant.

Un port 443 ouvert signifie-t-il que l'endpoint HTTPS fonctionne ?

Non. Un port ouvert confirme seulement que quelque chose a accepté une connexion TCP. La poignée de main TLS peut encore échouer parce que le certificat a expiré, que le nom d'hôte ne correspond pas ou que la chaîne intermédiaire est incomplète. Vérifiez le certificat séparément une fois le port confirmé ouvert.

À propos de l’auteur

Shaik Vahid

Fondateur de DNS Robot. Vahid a passé des années au support d’hébergement web à réparer les domaines, les enregistrements DNS, les certificats SSL et la distribution des e-mails de clients. Il développe les outils de DNS Robot et écrit ces guides à partir des erreurs que les gens rencontrent vraiment.

Vérificateur d'en-têtes HTTP

Analysez les en-têtes de réponse HTTP et la notation des en-têtes de sécurité