Интеграция8 мин чтения

Прокси в curl_cffi: авторизация, ошибка 407 и SOCKS

Настройка прокси в curl_cffi 0.16.3: учётные данные и impersonate, почему 407 на CONNECT даёт ConnectionError, а не ProxyError, правила HTTP_PROXY и socks5h.

На этой странице

Укажите учётные данные в URL прокси и передайте его в параметре proxy:

python
from curl_cffi import requests

PROXY = "http://user:pass@proxy.example.com:8080"

with requests.Session(impersonate="chrome", proxy=PROXY) as session:
    response = session.get("https://example.com/")
    print(response.status_code)

Чтобы поймать сбои прокси, перехватывайте ProxyError и ConnectionError и проверяйте status_code на 407. В curl_cffi 0.16 прокси, который отклоняет CONNECT, вызывает ConnectionError, а не ProxyError.

В нашей лаборатории такой код прошёл авторизацию для запросов https:// и http:// на локальном прокси, который требует Basic-авторизацию. Всё, что описано ниже, записано 10 октября 2026 года с curl_cffi 0.16.3 (libcurl 8.21.0) на Python 3.14.4. impersonate выбирает профиль браузера, у которого curl_cffi берёт настройки TLS и HTTP/2. Все результаты ниже были одинаковыми с этим параметром и без него.

Три детали перед разбором ошибок:

  • Используйте proxy или proxies, но не оба сразу. proxies={"http": PROXY, "https": PROXY} работал одинаково в отдельном запросе и в Session. Ключ — это схема запрашиваемого URL: когда в словаре был только ключ "https", запрос http:// ушёл напрямую. Передача обоих аргументов вызвала TypeError: Cannot specify both 'proxy' and 'proxies'.
  • Кодируйте пароль в URL процентным кодированием. p@ss:w/rd сработал в виде p%40ss%3Aw%2Frd. Без кодирования запрос завершился ProxyError с текстом curl: (5) Unsupported proxy syntax. proxy_auth=("user", "p@ss:w/rd") вместе с URL прокси без учётных данных тоже сработал, и кодировать его не нужно.
  • Пишите http:// для HTTP-прокси, в том числе для URL https://. Схема описывает соединение с прокси. URL прокси со схемой https://, направленный на обычный HTTP-прокси лаборатории, вызвал SSLError с curl: (35) TLS connect error и WRONG_VERSION_NUMBER. Это сообщение разобрано в руководстве SSL WRONG_VERSION_NUMBER через прокси.

CONNECT tunnel failed, response 407: ConnectionError, а не ProxyError

code
curl_cffi.requests.exceptions.ConnectionError: Failed to perform, curl: (7) CONNECT tunnel failed, response 407. See https://curl.se/libcurl/c/libcurl-errors.html first for more details.

Для URL https:// curl_cffi сначала отправляет прокси запрос CONNECT. Прокси лаборатории ответил 407, и запрос завершился этой ошибкой. Сообщение было одинаковым без учётных данных и с неверным паролем.

  • isinstance(error, ProxyError) возвращает False. ProxyError и ConnectionError — соседние классы, оба наследуют RequestException, поэтому except ProxyError эту ошибку не ловит.
  • error.code равен 7.
  • error.response — это Response, у которого status_code равен 0, а не 407. Статус есть только в тексте сообщения.
  • Код 7 не означает именно 407. Когда прокси лаборатории не смог соединиться с целевым сервером, он ответил 502, и сообщение было curl: (7) CONNECT tunnel failed, response 502. Когда порт прокси никто не слушал, оно было таким: curl: (7) Failed to connect to 127.0.0.1:8443 over proxy 127.0.0.1 after 0 ms: Could not connect to server.

Класс исключения определяется кодом ошибки curl, а этот код менялся от выпуска к выпуску. Мы выполнили один и тот же запрос на двенадцати выпусках:

curl_cffilibcurlОтклонённый CONNECTСбой SOCKS
0.16.0–0.16.38.21.0ConnectionError, код curl 7ProxyError, код curl 97
0.7.4–0.15.08.7.0–8.15.0ProxyError, код curl 56ProxyError, код curl 97

До версии 0.15.0 включительно сообщение было curl: (56) CONNECT tunnel failed, response 407. Утилита curl перешла с кода выхода 56 на 7 в версии 8.18.0. Эта таблица и порядок проверки учётных данных есть в руководстве Как исправить ошибку прокси 407.

Этот блок обрабатывает отклонённый CONNECT в версиях до и после изменения, сбой SOCKS и ответ 407:

python
import re

from curl_cffi.requests.exceptions import ConnectionError, ProxyError

CONNECT_STATUS = re.compile(r"CONNECT tunnel failed, response (\d{3})")


class ProxyFailure(Exception):
    pass


def fetch(session, url):
    try:
        response = session.get(url)
    except ProxyError as error:
        raise ProxyFailure(f"proxy error, curl {error.code}") from error
    except ConnectionError as error:
        rejected = CONNECT_STATUS.search(str(error))
        if error.code == 7 and rejected:
            raise ProxyFailure(f"proxy answered CONNECT with {rejected[1]}") from error
        raise
    if response.status_code == 407:
        raise ProxyFailure("proxy answered 407")
    return response

В лаборатории он выдал proxy answered CONNECT with 407, proxy answered CONNECT with 502, proxy answered 407 для URL http:// и proxy error, curl 97 для сбоя SOCKS. Для закрытого порта прокси он повторно выбросил ConnectionError.

407 для URL http://: ответ, а не исключение

Для URL http:// туннеля нет. curl_cffi отправляет прокси полный URL в обычном запросе GET, так он работает начиная с 0.7.0b7 (issue #326), и ответ 407 от прокси возвращается как обычный ответ.

Исключения не было. response.status_code был равен 407, response.ok — False, а в заголовках был Proxy-Authenticate: Basic realm="lab". Код, который только перехватывает исключения, примет это за успешный запрос с пустым телом.

response.raise_for_status() вызвал curl_cffi.requests.exceptions.HTTPError: HTTP Error 407: Proxy Authentication Required. У этого исключения error.response.status_code равен 407.

Переменные окружения: http_proxy читается, HTTP_PROXY — нет

Когда прокси в коде не задан, libcurl берёт его из окружения:

  • Для URL http:// переменная HTTP_PROXY в верхнем регистре была проигнорирована, и запрос ушёл напрямую. С http_proxy в нижнем регистре он прошёл через прокси.
  • Для URL https:// использовались и HTTPS_PROXY, и https_proxy. С одной лишь http_proxy запрос https:// ушёл напрямую.
  • ALL_PROXY и all_proxy действовали для обеих схем.
  • С NO_PROXY=127.0.0.1 и с no_proxy в нижнем регистре запрос ушёл напрямую.

libcurl описывает это исключение в документации: переменные проверяются и в верхнем регистре, "except the http_proxy one which is only used lowercase" (переменные окружения libcurl). Причина в CGI: сервер превращает заголовок запроса Proxy: в переменную HTTP_PROXY для скрипта (Everything curl).

Scrapy читает HTTP_PROXY в верхнем регистре (Прокси в Scrapy 2.19), а aiohttp не читает ни одной переменной, пока вы не передадите trust_env=True (Прокси в aiohttp 3.14). Другие клиенты сравниваются в руководстве Переменные окружения прокси.

Два результата легко упустить:

  • NO_PROXY имеет приоритет над аргументом proxy. С переменной NO_PROXY=127.0.0.1 запрос requests.get(url, proxy=PROXY) ушёл напрямую для обеих схем. Когда мы добавили curl_options={CurlOpt.NOPROXY: ""}, он снова прошёл через прокси. Над http_proxy аргумент имел приоритет.
  • trust_env=False не отключил переменные. Session(trust_env=False) по-прежнему использовал http_proxy и HTTPS_PROXY. proxy="" тоже их не отключил.

Две настройки отправили запрос напрямую при заданных переменных:

python
from curl_cffi import CurlOpt, requests

requests.get("https://example.com/", proxies={"all": ""})
requests.get("https://example.com/", curl_options={CurlOpt.NOPROXY: "*"})

Обе работают и как аргументы Session. Пустая строка прокси "explicitly disables the use of a proxy, even if there is an environment variable set for it" (CURLOPT_PROXY).

SOCKS: socks5:// резолвит локально, socks5h:// отправляет имя хоста

Лаборатория запрашивала http://dnsprobe.example:8080/ — имя, которое есть только в файле hosts как 127.0.0.2, — через SOCKS5-сервер, который записывает каждое место назначения:

  • С socks5://127.0.0.1:1080 сервер записал CONNECT atyp=IPv4 dest=127.0.0.2:8080. curl_cffi сам разрешил имя.
  • С socks5h://127.0.0.1:1080 он записал CONNECT atyp=DOMAIN dest=dnsprobe.example:8080. Прокси получил имя хоста.

Для URL https:// результаты были теми же. Это соглашение самого curl (Прокси в curl). Клиенты, которые ему не следуют, перечислены в статье socks5 и socks5h.

Неудачное рукопожатие SOCKS — это тот случай, когда ProxyError действительно возникает:

code
curl_cffi.requests.exceptions.ProxyError: Failed to perform, curl: (97) cannot complete SOCKS5 connection to 127.0.0.1. (1). See https://curl.se/libcurl/c/libcurl-errors.html first for more details.

error.code равен 97. Последнее число в скобках — код ответа, который прислал SOCKS-сервер: сервер лаборатории ответил 1, общий сбой. Когда порт целевого сервера был закрыт и сервер ответил 5, сообщение заканчивалось на (5).

Схема, которая не соответствует порту, даёт другие ошибки:

  • socks5://, направленный на порт HTTP-прокси, вызвал ProxyError с curl: (97) Received invalid version in initial SOCKS5 response.
  • http://, направленный на порт SOCKS, вызвал ProxyError с curl: (56) Proxy CONNECT aborted для URL https://.
  • Когда порт SOCKS никто не слушал, снова возник ConnectionError с кодом 7.

AsyncSession: та же настройка, те же ошибки

python
import asyncio

from curl_cffi import AsyncSession

PROXY = "http://user:pass@proxy.example.com:8080"


async def main():
    async with AsyncSession(impersonate="chrome", proxy=PROXY) as session:
        response = await session.get("https://example.com/")
        print(response.status_code)


asyncio.run(main())

Каждый асинхронный случай совпал со своим синхронным аналогом. Отклонённый CONNECT вызвал тот же ConnectionError с кодом 7. Для URL http:// ответ 407 вернулся как обычный ответ. Сбой SOCKS вызвал ProxyError с кодом 97. socks5:// и socks5h:// отправили те же типы адресов, а HTTP_PROXY была проигнорирована, тогда как http_proxy использовалась.

Повторы: retry повторяет запросы POST

По умолчанию curl_cffi запросы не повторяет. Лаборатория отправила POST через прокси на цель, которая читает запрос и закрывает соединение без ответа. Вызов завершился ConnectionError с curl: (52) Empty reply from server, а цель получила один POST.

С requests.Session(proxy=PROXY, retry=1) тот же вызов вернул 201, а цель получила тело дважды. retry повторил POST так же, как повторяет GET.

retry=2 при отклонённом CONNECT отправил три запроса CONNECT и вызвал ту же ошибку, и с Session, и с AsyncSession. Повтор не исправляет неверные учётные данные.

Для запросов, которые небезопасно повторять, оставляйте retry равным 0 или отправляйте ключ идемпотентности. Что меняет такой ключ, показано в статье Повторы через прокси: один потерянный ответ, две созданные задачи. Для помощи с диагностикой внутри агента для написания кода Proxy Toolkit MCP принимает очищенные описания ошибок. Не передавайте учётные данные в его аргументах.

Как это проверялось

На Ubuntu 26.04.1 с Python 3.14.4 и curl_cffi 0.16.3 (libcurl 8.21.0-IMPERSONATE с BoringSSL), всё на loopback-адресах: python -m http.server в роли HTTP- и HTTPS-целей, три прокси, которые требуют Basic-авторизацию и записывают каждый запрос, SOCKS5-сервер, который записывает каждое место назначения, и ещё один, который на каждый запрос отвечает кодом 1, а также цель, которая теряет ответ на первый POST. Имя dnsprobe.example существовало только в отдельной копии файла hosts. Каждый из 86 случаев выполнялся в новом интерпретаторе с чистым окружением. Для сравнения версий были установлены двенадцать выпусков от 0.7.4 до 0.16.3. Аккаунт прокси-сервиса не использовался.

Архив лаборатории содержит скрипт запуска, случаи, прокси, SOCKS5-сервер, README, записанные результаты и сравнение версий.

Не проверялось: macOS и Windows, авторизация прокси по Digest и NTLM, SOCKS-прокси с авторизацией, прокси, до которых нужно подключаться по TLS, HTTP/3 и WebSocket.

ipvolt — прокси-сервис для разработчиков, который пока не открыт; запишитесь в список раннего доступа, и вы получите одно письмо, когда он откроется.

Источники и дополнительное чтение

Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.