Укажите учётные данные в URL прокси и передайте его в параметре proxy:
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-прокси, в том числе для URLhttps://. Схема описывает соединение с прокси. URL прокси со схемойhttps://, направленный на обычный HTTP-прокси лаборатории, вызвалSSLErrorсcurl: (35) TLS connect errorиWRONG_VERSION_NUMBER. Это сообщение разобрано в руководстве SSL WRONG_VERSION_NUMBER через прокси.
CONNECT tunnel failed, response 407: ConnectionError, а не ProxyError
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_cffi | libcurl | Отклонённый CONNECT | Сбой SOCKS |
|---|---|---|---|
| 0.16.0–0.16.3 | 8.21.0 | ConnectionError, код curl 7 | ProxyError, код curl 97 |
| 0.7.4–0.15.0 | 8.7.0–8.15.0 | ProxyError, код curl 56 | ProxyError, код curl 97 |
До версии 0.15.0 включительно сообщение было curl: (56) CONNECT tunnel failed, response 407. Утилита curl перешла с кода выхода 56 на 7 в версии 8.18.0. Эта таблица и порядок проверки учётных данных есть в руководстве Как исправить ошибку прокси 407.
Этот блок обрабатывает отклонённый CONNECT в версиях до и после изменения, сбой SOCKS и ответ 407:
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=""тоже их не отключил.
Две настройки отправили запрос напрямую при заданных переменных:
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 действительно возникает:
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для URLhttps://.- Когда порт SOCKS никто не слушал, снова возник
ConnectionErrorс кодом 7.
AsyncSession: та же настройка, те же ошибки
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 — прокси-сервис для разработчиков, который пока не открыт; запишитесь в список раннего доступа, и вы получите одно письмо, когда он откроется.
Источники и дополнительное чтение
Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.
- curl_cffi documentation: Proxies
- curl_cffi API reference: Session (proxy, proxies, proxy_auth, trust_env, retry)
- curl_cffi API reference: AsyncSession
- curl_cffi API reference: Exceptions and warnings (ProxyError, ConnectionError)
- curl_cffi documentation: Using CURLOPT_* in requests API (curl_options)
- curl_cffi issue #326: CONNECT was sent for http:// URLs, changed in 0.7.0b7
- libcurl: CURLOPT_PROXY (schemes, the empty string, socks5h)
- libcurl: CURLOPT_NOPROXY and the no_proxy variable
- libcurl environment variables: http_proxy is only used lowercase
- Everything curl: proxy environment variables and the CGI reason
- libcurl error codes: CURLE_COULDNT_CONNECT (7), CURLE_RECV_ERROR (56), CURLE_PROXY (97)