curl возвращает 403, но в браузере работает: найдите недостающий элемент
Браузер загружает страницу. curl получает 403. Разрыв между этими двумя запросами всегда конечен и всегда обнаружим — вот как его разделить за пять минут.
Вы вставляете URL в Chrome, и страница загружается. Вы вставляете тот же URL в curl и получаете 403 Forbidden. Ничего в ресурсе не изменилось за эти две секунды, поэтому разница заключается исключительно в запросе — а запрос это конечная, поддающаяся проверке вещь. Это руководство — процедура разделения: воспроизведите точно то, что отправил браузер, затем удаляйте части, пока 403 не вернется. То, что вы удалили последним, и есть ваш ответ. Подозреваемые, в порядке их обычной виновности: User-Agent, Referer, cookies, TLS fingerprint и JavaScript.
Шаг 0: докажите, что запросы действительно разные
Прежде чем строить теории, посмотрите, что на самом деле отправляет curl. С помощью -v вы увидите строку запроса, каждый заголовок и рукопожатие TLS. Запрос по умолчанию в curl удивительно тонкий — обычно Host, User-Agent: curl/8.x и Accept: */*. Браузер отправляет дюжину дополнительных заголовков.
# 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
Читайте заголовки ответа так же внимательно, как и строку состояния. Один из них решает вопрос сразу в большинстве случаев: Vary: User-Agent означает, что сервер намеренно выдает разные ответы в зависимости от того, кем вы утверждаете себя. В хорошо задокументированном случае на Stack Overflow curl -f против простого хоста Apache 2.4.38 вернул 403, в то время как wget получил идентичный файл с 200 — и успешный ответ содержал именно этот заголовок Vary: User-Agent. Передача -A 'Wget/1.21.2' в curl мгновенно исправила это. Владелец сайта заблокировал user agent curl после злоупотребления; ничего другого в запросе не имело значения.
Пока вы читаете вывод: curl: (22) The requested URL returned error: 403 не является отдельной проблемой. Код выхода 22 — это то, что делает -f/--fail с любой HTTP-ошибкой — флаг подавляет тело и завершает команду с ошибкой. Временно уберите -f, чтобы вы могли действительно прочитать блокирующую страницу, которая обычно называет систему, которая вас остановила.
Шаг 1: Copy as cURL, 30-секундный ответ
Оба основных браузера могут предоставить вам точный запрос, который они только что сделали. Откройте DevTools, перейдите на вкладку Network, щелкните правой кнопкой мыши запрос и выберите "Copy as cURL". Chrome поддерживает это с версии 26, а Firefox с версии 31, и вывод включает каждый заголовок, каждый cookie и referer. Вставьте это в свой терминал: если он возвращает 200, ваша проблема определенно в форме запроса, и шаг 2 определит, какая часть.
Одна ловушка здесь тратит много времени. Если URL перенаправляется, панель Network очищается при навигации, и вы копируете неправильный запрос. Установите "Preserve log" в Chrome или "Persistent Logs" в Firefox сначала, чтобы вы могли видеть как запрос, который перенаправил, так и тот, который в итоге обслужил контент. Цепочки перенаправлений имеют значение: в известной теме на Unix Stack Exchange сервер проверял Referer, затем перенаправлял через 302 на местоположение, которое ничего не проверяло — что делало сбой случайным, пока не была видна вся цепочка.

Шаг 2: разделите заголовки
Начните с работающей команды "Copy as cURL" и удаляйте заголовки один за другим, перезапуская после каждого удаления. Первое удаление, которое возвращает 403, называет вашего виновника. На практике это почти всегда один из четырех.
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) — заблокирован напрямую или сервер ветвится на нем. Проверьте текущую строку Chrome, затем wget, затем бессмысленную; паттерн результатов подскажет, сталкиваетесь ли вы с черным списком или списком разрешений. - Referer (
-e) — активы и ссылки на загрузку часто возвращают 403, если запрос не выглядит так, как будто он пришел с собственной страницы сайта. Заголовок является необязательным по спецификации, именно поэтому люди о нем забывают. - Cookies (
-b) — cookie согласия, сессии или антибота, установленный на предыдущей странице. Подтвердите это за секунды: откройте URL в приватном окне. Если браузер также возвращает 403, cookies — ваш ответ. - Authorization (
-u, или заголовок bearer) — подписанные или токенизированные URL часто возвращают 403, когда их копируют вне контекста, потому что токен был привязан к сессии или уже истек.
Два завершающих детали для этого уровня. Заключите URL в кавычки: строка запроса, содержащая & или токен доступа, будет искажена вашим shell, и результатом будет 403, не связанный с сервером. И если вы отлаживаете из PHP или Node, а не из shell, воспроизведите тот же набор заголовков там — значения по умолчанию libcurl внутри PHP отличаются от командного инструмента, что объясняет, почему идентичный запрос может пройти в терминале и не пройти в коде. Наш curl proxy recipes охватывает синтаксис флагов полностью.
Шаг 3: когда идентичные заголовки все равно возвращают 403
Если побайтовая копия заголовков браузера все равно не удается, решение было принято до того, как ваши заголовки были проанализированы. Под ними находятся два уровня.
TLS fingerprint. Ваш ClientHello — наборы шифров, расширения, предпочтения кривых, ALPN, плюс кадр настроек HTTP/2, который следует за ним — хэшируется в значение JA3 или JA4. curl, собранный с OpenSSL, производит один, который ни один браузер никогда не производит, и антибот-системы сравнивают его с вашим заявленным User-Agent. Заявление о том, что вы Chrome, при рукопожатии как OpenSSL — это противоречие, которое они созданы для обнаружения. Исправление — клиент, который воспроизводит рукопожатия браузера: curl-impersonate в командной строке или curl_cffi из 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"))
Ваш IP. Браузер, который работает, обычно находится на вашем домашнем подключении, в то время как curl работает на VPS. Хостинговые ASN публикуются и предварительно оцениваются, поэтому тот же запрос с домашнего адреса оценивается иначе, прежде чем он даже будет прочитан. Замена выхода — это изменение в одну строку с residential proxies — более 90 миллионов IP-адресов в более чем 200 странах, HTTP и SOCKS5 в каждом плане — и это самый быстрый способ исключить сеть. Механика уровня отпечатков пальцев описана в JA3/JA4 TLS fingerprinting.
Исключите сеть с помощью residential proxies

Шаг 4: странице нужен браузер, а не клиент
Иногда 403 — это не суждение о вас вообще — это режим отказа вызова, который вы никогда не пытались. Обсуждение на GitHub о проверяющих ссылках, попадающих на npmjs.com, говорит об этом прямо: curl не может создать действительное решение вызова, поэтому запрос блокируется с 403. Сервер выдает небольшую проблему на JavaScript, ждет момент для ответа и отказывает всему, что не может ее выполнить. Никакой набор заголовков, никакой отпечаток пальца и никакой IP не проходят тест, требующий выполнения кода.
В этот момент у вас есть три честных варианта: запустить настоящий браузер и оплатить стоимость, найти JSON endpoint, который сама страница вызывает (часто находящийся на той же вкладке Network, которую вы уже открыли), или передать URL сервису, который рендерит по запросу. QuantumProxies Scraper API делает последнее — TLS уровня браузера, выходы residential, рендеринг JavaScript только там, где это нужно странице, и markdown, JSON или raw HTML в ответ на один запрос. Если то, что вы получаете, это пустая страница, а не запрещенная, это другая диагностика: см. почему ваш scraper возвращает пустую страницу. И если страница блокировки содержит Cloudflare Ray ID, перейдите к Cloudflare error 1020.
Часто задаваемые вопросы
Почему curl получает 403, когда мой браузер не получает?
Потому что curl отправляет примерно три заголовка, без cookies, без referer и отпечаток TLS, не относящийся к браузеру, в то время как ваш браузер отправляет дюжину заголовков, банку cookies и рукопожатие Chrome. Сервер отказывает в запросе, а не в ресурсе. Воспроизведите точный запрос браузера с помощью "Copy as cURL", затем удаляйте заголовки один за другим, чтобы найти, какая разница имеет значение.
Почему wget успешен там, где curl получает 403?
Почти всегда это User-Agent. Некоторые серверы специально блокируют UA curl после злоупотребления, оставляя UA wget нетронутым — задокументированный случай показал заголовок ответа Vary: User-Agent, подтверждающий, что сервер ветвится на нем, и curl -A 'Wget/1.21.2' восстановил 200. wget также отправляет Accept-Encoding и Connection по умолчанию, что иногда тоже имеет значение.
Как установить User-Agent в curl?
Используйте -A 'string' или эквивалент -H 'User-Agent: string'. Предпочтите полную, актуальную строку браузера вместо усеченной Mozilla/5.0, которую некоторые серверы теперь отклоняют именно потому, что ни один реальный браузер не отправляет только два токена. Сопоставьте это с соответствующими значениями Accept и Accept-Language, чтобы весь набор оставался согласованным.
Что означает ошибка curl 22?
Код выхода 22 создается -f/--fail всякий раз, когда сервер возвращает HTTP-ошибку, и сообщение цитирует статус — обычно 403. Это флаг отчетности, а не отдельная ошибка. Уберите -f, чтобы увидеть тело ответа, которое обычно объясняет блокировку гораздо лучше, чем код выхода.
Может ли прокси исправить curl 403?
Он исправляет подмножество, вызванное репутацией IP или географией — большое подмножество, когда ваш скрипт работает на облачном хосте, а ваш браузер нет. Это не исправит отсутствующий referer, отсутствующий cookie или вызов JavaScript. Сначала проверьте заголовки, так как они ничего не стоят, затем измените выходной IP, чтобы изолировать сетевой слой.
Здесь нет тайны, только разрыв: браузер отправил один запрос, а вы отправили другой. Скопируйте запрос браузера, уменьшите его, пока он не сломается, и вы всегда найдете часть, которая имела значение — обычно это заголовок, иногда отпечаток пальца, иногда вызов, который требует настоящего браузера для ответа.