curl renvoie 403 mais le navigateur fonctionne : trouvez la pièce manquante
Le navigateur le charge. curl obtient 403. L'écart entre ces deux requêtes est toujours fini et toujours trouvable — voici comment le diviser en cinq minutes.
Vous collez une URL dans Chrome et la page se charge. Vous collez la même URL dans curl et obtenez 403 Interdit. Rien n'a changé concernant la ressource entre ces deux secondes, donc la différence réside entièrement dans la requête — et une requête est une chose finie et inspectable. Ce guide est une procédure de bisection : rejouez exactement ce que le navigateur a envoyé, puis retirez des éléments jusqu'à ce que le 403 revienne. Ce que vous avez retiré en dernier est votre réponse. Les suspects, dans l'ordre où ils sont généralement coupables, sont User-Agent, Referer, cookies, empreinte TLS et JavaScript.
Étape 0 : prouvez que les requêtes sont vraiment différentes
Avant de théoriser, regardez ce que curl envoie réellement. Avec -v vous voyez la ligne de requête, chaque en-tête et la poignée de main TLS. Une requête curl par défaut est étonnamment mince — typiquement Host, User-Agent: curl/8.x et Accept: */*. Un navigateur envoie une douzaine de plus.
# what you send, what you get back, and the TLS details
curl -v -o /dev/null https://target.example/page
# just the response headers, quickly
curl -sS -o /dev/null -D - https://target.example/page
Lisez les en-têtes de réponse aussi attentivement que la ligne de statut. L'un d'eux règle la question d'emblée dans le cas le plus courant : Vary: User-Agent signifie que le serveur sert délibérément des réponses différentes selon qui vous prétendez être. Dans un cas bien documenté sur Stack Overflow, curl -f contre un hôte Apache 2.4.38 renvoyait 403 tandis que wget récupérait le fichier identique avec un 200 — et la réponse réussie portait exactement cet en-tête Vary: User-Agent. Passer -A 'Wget/1.21.2' à curl l'a corrigé instantanément. Le propriétaire du site avait mis sur liste noire l'agent utilisateur de curl après des abus ; rien d'autre dans la requête n'avait d'importance.
Pendant que vous lisez la sortie : curl: (22) L'URL demandée a renvoyé l'erreur : 403 n'est pas un problème distinct. Le code de sortie 22 est ce que -f/--fail fait avec toute erreur HTTP — le drapeau supprime le corps et échoue la commande. Supprimez temporairement -f pour pouvoir réellement lire la page de blocage, qui nomme généralement le système qui vous a arrêté.
Étape 1 : Copier en tant que cURL, la réponse en 30 secondes
Les deux principaux navigateurs peuvent vous fournir la requête exacte qu'ils viennent de faire. Ouvrez DevTools, allez à l'onglet Réseau, cliquez avec le bouton droit sur la requête et choisissez "Copier en tant que cURL". Chrome propose cela depuis la version 26 et Firefox depuis la 31, et la sortie inclut chaque en-tête, chaque cookie et le referer. Collez-le dans votre terminal : s'il renvoie 200, votre problème réside définitivement dans la forme de la requête, et l'étape 2 trouve quelle partie.
Un piège fait perdre beaucoup de temps ici. Si l'URL redirige, le panneau Réseau se vide lors de la navigation et vous copiez la mauvaise requête. Cochez "Préserver le journal" dans Chrome ou "Journaux persistants" dans Firefox d'abord, afin de voir à la fois la requête qui a redirigé et celle qui a finalement servi le contenu. Les chaînes de redirection comptent : dans un fil bien connu de Unix Stack Exchange, le serveur vérifiait le Referer, puis passait par un 302 vers un emplacement qui ne vérifiait rien du tout — ce qui rendait l'échec aléatoire jusqu'à ce que toute la chaîne soit visible.

Étape 2 : divisez les en-têtes
Commencez par la commande "Copier en tant que cURL" qui fonctionne et supprimez les en-têtes un par un, en relançant après chaque suppression. La première suppression qui ramène le 403 nomme votre coupable. En pratique, c'est presque toujours l'un des quatre.
curl -sS -o /dev/null -w '%{http_code}\n' \
-A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-GB,en;q=0.9' \
-e 'https://target.example/' \
-b 'session=abc123; consent=1' \
-L \
'https://target.example/page'
- User-Agent (
-A) — bloqué directement, ou le serveur s'y branche. Testez une chaîne Chrome actuelle, puis celle de wget, puis une absurde ; le schéma des résultats vous indique si vous êtes sur une liste noire ou une liste blanche. - Referer (
-e) — les actifs et les liens de téléchargement renvoient souvent 403 à moins que la requête ne semble provenir de la page du site. L'en-tête est facultatif selon la spécification, c'est exactement pourquoi les gens l'oublient. - Cookies (
-b) — un consentement, une session ou un cookie anti-bot défini sur une page précédente. Confirmez-le en quelques secondes : ouvrez l'URL dans une fenêtre privée. Si le navigateur renvoie également 403 là-bas, les cookies sont votre réponse. - Authorization (
-u, ou un en-tête bearer) — les URL signées ou tokenisées renvoient fréquemment 403 lorsqu'elles sont copiées hors contexte, car le jeton était lié à une session ou a déjà expiré.
Deux détails de clôture pour ce niveau. Citez l'URL : une chaîne de requête contenant & ou un jeton d'accès est déformée par votre shell autrement, et le 403 résultant n'a rien à voir avec le serveur. Et si vous déboguez depuis PHP ou Node plutôt que le shell, reproduisez le même ensemble d'en-têtes là-bas — les valeurs par défaut de libcurl à l'intérieur de PHP diffèrent de celles de l'outil en ligne de commande, c'est pourquoi la requête identique peut passer dans un terminal et échouer dans le code. Nos recettes de proxy curl couvrent la syntaxe des drapeaux en entier.
Étape 3 : lorsque des en-têtes identiques renvoient toujours 403
Si une copie octet par octet des en-têtes du navigateur échoue toujours, la décision a été prise avant que vos en-têtes ne soient analysés. Deux couches se trouvent en dessous.
Empreinte TLS. Votre ClientHello — suites de chiffrement, extensions, préférences de courbe, ALPN, plus le cadre de paramètres HTTP/2 qui suit — se hache en une valeur JA3 ou JA4. curl construit avec OpenSSL en produit une qu'aucun navigateur ne produit jamais, et les systèmes anti-bot la comparent à votre User-Agent revendiqué. Prétendre être Chrome tout en négociant comme OpenSSL est une contradiction qu'ils sont conçus pour détecter. La solution est un client qui reproduit les poignées de main des navigateurs : curl-impersonate en ligne de commande, ou curl_cffi depuis Python.
# pip install curl_cffi
from curl_cffi import requests
proxy = "http://USER:PASS@gate.quantumproxies.io:8000"
r = requests.get(
"https://target.example/page",
impersonate="chrome", # browser ClientHello + HTTP/2 settings
proxies={"http": proxy, "https": proxy},
timeout=20,
)
print(r.status_code, r.headers.get("content-type"))
Votre IP. Le navigateur qui fonctionne est généralement sur votre connexion domestique tandis que curl fonctionne sur un VPS. Les ASN d'hébergement sont publiés et pré-évalués, donc la même requête depuis une adresse résidentielle est jugée différemment avant même d'être lue. Changer la sortie est un changement en une ligne avec des proxies résidentiels — plus de 90 millions d'IPs dans plus de 200 pays, HTTP et SOCKS5 sur chaque plan — et c'est le moyen le plus rapide d'exclure le réseau. Les mécanismes de la couche d'empreinte sont dans l'empreinte TLS JA3/JA4.
Éliminez le réseau avec des proxies résidentiels

Étape 4 : la page nécessite un navigateur, pas un client
Parfois, le 403 n'est pas un jugement sur vous du tout — c'est le mode d'échec d'un défi que vous n'avez jamais tenté. Une discussion publique sur GitHub à propos des vérificateurs de liens frappant npmjs.com le dit clairement : curl ne peut pas produire une solution de défi valide, donc la requête est bloquée avec un 403. Le serveur émet un petit problème JavaScript, attend un moment pour la réponse, et refuse tout ce qui ne peut pas l'exécuter. Aucun ensemble d'en-têtes, aucune empreinte et aucune IP ne passe un test qui nécessite l'exécution de code.
À ce stade, vous avez trois options honnêtes : piloter un vrai navigateur et en payer le coût, trouver le point de terminaison JSON que la page elle-même appelle (souvent situé dans le même onglet Réseau que vous avez déjà ouvert), ou confier l'URL à un service qui rend à la demande. L'API de Scraping de QuantumProxies fait la dernière — TLS de qualité navigateur, sorties résidentielles, rendu JavaScript uniquement là où une page en a besoin, et markdown, JSON ou HTML brut en retour d'une seule requête. Si ce que vous obtenez est une page vide plutôt qu'une interdite, c'est un diagnostic différent : voir pourquoi votre scraper renvoie une page vide. Et si la page de blocage porte un ID Cloudflare Ray, allez à Cloudflare erreur 1020 à la place.
Questions fréquemment posées
Pourquoi curl obtient-il 403 alors que mon navigateur ne le fait pas ?
Parce que curl envoie environ trois en-têtes, pas de cookies, pas de referer et une empreinte TLS non-navigateur, tandis que votre navigateur envoie une douzaine d'en-têtes, un ensemble de cookies et une poignée de main Chrome. Le serveur refuse la requête, pas la ressource. Rejouez la requête exacte du navigateur avec "Copier en tant que cURL", puis retirez les en-têtes un par un pour trouver quelle différence compte.
Pourquoi wget réussit-il là où curl obtient 403 ?
Presque toujours l'User-Agent. Certains serveurs mettent sur liste noire l'UA de curl spécifiquement après des abus tout en laissant celui de wget intact — un cas documenté montrait un en-tête de réponse Vary: User-Agent confirmant que le serveur s'y branche, et curl -A 'Wget/1.21.2' a restauré le 200. wget envoie également par défaut Accept-Encoding et Connection, ce qui compte parfois aussi.
Comment définir un User-Agent dans curl ?
Utilisez -A 'chaîne', ou l'équivalent -H 'User-Agent: chaîne'. Préférez une chaîne de navigateur complète et actuelle à un Mozilla/5.0 tronqué, que certains serveurs rejettent désormais précisément parce qu'aucun vrai navigateur n'envoie seulement deux jetons. Associez-le à des valeurs Accept et Accept-Language correspondantes pour que l'ensemble reste cohérent.
Que signifie l'erreur curl 22 ?
Le code de sortie 22 est produit par -f/--fail chaque fois que le serveur renvoie une erreur HTTP, et le message cite le statut — généralement 403. C'est un drapeau de rapport, pas une faute distincte. Supprimez -f pour voir le corps de la réponse, qui explique généralement le blocage bien mieux que le code de sortie.
Un proxy peut-il corriger un curl 403 ?
Il corrige le sous-ensemble causé par la réputation ou la géographie de l'IP — un grand sous-ensemble lorsque votre script s'exécute sur un hôte cloud et que votre navigateur ne le fait pas. Il ne corrigera pas un referer manquant, un cookie absent ou un défi JavaScript. Testez d'abord les en-têtes, car ils ne coûtent rien, puis changez l'IP de sortie pour isoler la couche réseau.
Il n'y a pas de mystère ici, seulement un écart : le navigateur a envoyé une requête et vous en avez envoyé une autre. Copiez celle du navigateur, réduisez-la jusqu'à ce qu'elle échoue, et vous trouverez toujours la pièce qui comptait — généralement un en-tête, parfois une empreinte, occasionnellement un défi qui nécessite un vrai navigateur pour répondre.