Прокси в HTTPX: авторизация, переменные окружения и тайм-ауты

11 мин чтения

Как задать proxy= в HTTPX 0.28, отличить Basic шлюза от auth= цели, разобрать HTTP_PROXY и trust_env, понять ReadTimeout на заголовках и на теле.

HTTPX ходит в сеть и через прокси, и напрямую. Ниже рабочая настройка для установленной 0.28.1: параметр proxy=, Basic на шлюзе, переменные окружения и раздельные тайм-ауты. Бесплатных ключей нет, адреса мы не продаём. Лаборатория крутится на loopback. Она не заменяет проверку боевого выхода в прокси-чекере и на странице мой IP.

01Как направить запрос через proxy=

В 0.28.1 словарь proxies= уже не принимается. Документация Proxies для этой версии задаёт шлюз параметром proxy= у Client и у httpx.get. Старый аргумент даёт TypeError, а не тихий откат на прямое соединение.

Один URL шлюза закрывает и HTTP, и HTTPS. Для схемы цели https:// URL самого прокси почти всегда остаётся http://.... Это не опечатка в той же странице: клиент сначала открывает обычный TCP к шлюзу, затем для TLS делает CONNECT.

Подставьте свою сборку, сохраните файл и запустите его. Ниже плейсхолдеры вашего шлюза и цели, не лабораторные порты фикстуры.

python3 -m pip install "httpx[socks]==0.28.1"
# save as proxy_get.py, then: python3 proxy_get.py
import httpx

PROXY_URL = "http://USER:PASSWORD@proxy.example:8080"  # ваш HTTP-шлюз
TARGET_URL = "https://example.com/"  # цель, которую вы контролируете

with httpx.Client(proxy=PROXY_URL, timeout=8.0, trust_env=False) as client:
    r = client.get(TARGET_URL)
    print(r.status_code)
  1. Поставьте httpx==0.28.1. В этой сборке inspect.signature(httpx.Client.__init__) содержит proxy и не содержит proxies.
  2. Подставьте PROXY_URL и TARGET_URL своими значениями. Логин спецсимволами кодируйте через quote(..., safe='').
  3. Держите клиент в with. Иначе пул и транспорт живут дольше, чем вы думаете.
  4. Смотрите не только статус 200. Прямой запрос к origin тоже даёт 200. В лаборатории origin писал заголовок X-Lab-Proxy, и только явный proxy= оставлял там alpha.
Сводка явного proxy= против прямого запроса

Прямой GET на loopback-origin вернул 200 без X-Lab-Proxy. Тот же путь через Client(proxy=...) вернул 200 и метку alpha. Статус сам по себе маршрут не доказывает. Браузерная страница «мой IP» тоже не доказывает маршрут библиотеки: у неё свой стек.

Если нужны разные шлюзы на http:// и https://, документация просит mounts с отдельными HTTPTransport. Настройки клиента в mounted-транспорт сами не переезжают. Прокси, trust_env и SSL-контекст задают на транспорте, затем транспорт закрывают.

02Как открыть HTTPS через HTTP-прокси

Для HTTPS-цели HTTPX не «форвардит HTTPS как есть». Раздел FORWARD vs TUNNEL описывает CONNECT, затем TLS уже с origin. Поддельный ответ CONNECT без живого туннеля в лаборатории отвергали: успешный случай читал JSON origin с полем origin=https-origin.

  1. Возьмите HTTP-прокси, даже если цель https://.
  2. Для HTTPS к своему origin с частным CA соберите контекст из файла, которым вы владеете: ssl.create_default_context(cafile=CA_FILE) и передайте его в verify=. Не ставьте verify=False.
  3. Убедитесь, что шлюз умеет туннель, а не отвечает заглушкой.
  4. Проверьте, что Proxy-Authorization не уехал на origin. В прогоне поле has_proxy_authorization на HTTPS-origin осталось ложным.

Без нашего CA прямой HTTPS к лабораторному серверу упал цепочкой httpx.ConnectError → httpcore.ConnectError → ssl.SSLCertVerificationError. С явным контекстом тот же хост ответил 200. Это проверка сертификата, не «прокси сломался».

HTTPTransport(retries=...) по странице Transports повторяет только ConnectError и ConnectTimeout. На отказ CONNECT из-за пароля это не похоже, и 429/503 он не разгребает. В отказе соединения мы ставили retries=0, чтобы прогон не крутился.

SOCKS в 0.28.1 ставится схемой socks5:// или socks5h:// после pip install "httpx[socks]==0.28.1", как в том же разделе SOCKS. В нашей сборке socksio==1.0.0. Оба запроса на hostname httpx-lab.test дошли как SOCKS CONNECT с ATYP DOMAIN. Это не внешняя утечка DNS: имя резолвилось подставленным getaddrinfo в той же лаборатории. Успешный SOCKS сам по себе не показывает, где резолвился бы чужой хост в проде.

03Как задать логин и пароль прокси

Пароль шлюза живёт в userinfo URL либо в httpx.Proxy(..., auth=(user, password)). Это не auth= запроса. auth= вешает Authorization на origin. Прокси ждёт Proxy-Authorization и не должен отдавать его цели.

from urllib.parse import quote
import httpx

user = "PROXY_USER"
password = "PROXY_PASSWORD"
host = "proxy.example"
port = 8080
proxy = f"http://{quote(user, safe='')}:{quote(password, safe='')}@{host}:{port}"

with httpx.Client(proxy=proxy, trust_env=False) as client:
    r = client.get("https://example.com/")

Спецсимволы кодируйте по компонентам, safe=''. Иначе двоеточие в пароле разрежет userinfo. В лаборатории закодированная пара после quote дала 200 и метку alpha.

  1. Соберите URL шлюза без логина, если хотите отдать пару в httpx.Proxy.auth. Конструктор 0.28.1 принимает auth кортежем.
  2. Прогоните три случая отдельно: нет пароля, неверный, верный. Не склеивайте их одним «если 407».
  3. Для HTTP-форварда наша фикстура ответила обычным 407 и на пустой, и на неверный Basic.
  4. Для HTTPS CONNECT те же фикстурные 407 HTTPX поднял как httpx.ProxyError над httpcore.ProxyError. Это не тот же объект, что статус 407 у форварда.
  5. Отдельным запросом передайте auth= цели. Origin увидел Authorization и не увидел Proxy-Authorization.
CONNECT: нет пароля, неверный, верный

Не пишите сырой Basic и URL с паролем в логи. Гарнесс краснеет userinfo до ***:***@. Скриншот терминала с живым паролем для статьи не годится.

Резидентный выход и логин к сайту это разные оси. Протокол шлюза не обещает «страну аккаунта». Лабораторный 200 на loopback не говорит, пустит ли вас чужой сервис.

04Как работают HTTPPROXY, NOPROXY и trustenv

По умолчанию HTTPX читает среду. Environment Variables называет HTTP_PROXY, HTTPS_PROXY, ALL_PROXY и NO_PROXY, плюс SSL_CERT_FILE и SSL_CERT_DIR. Выключить чтение можно флагом trust_env=False на клиенте или на вызове верхнего уровня.

Это не «только прокси». Тот же флаг решает, подхватит ли клиент файл CA из среды. Netrc в 0.28.1 сам по себе не включается, пока вы не передадите NetRCAuth, но документация среды всё равно про CA. В дочернем процессе лаборатория чистила верхний и нижний регистр прокси-переменных, CGI-поля, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, SSL_CERT_* и SSLKEYLOGFILE. HOME не трогали. Для loopback, который обязан идти через прокси, в ребёнка ставили NO_PROXY="", иначе системный обход localhost прячет сломанный шлюз.

Два шлюза в лаборатории отвечали разными именами alpha и beta. Приоритет смотрели по этой метке, не по коду 200.

  1. Чистый ребёнок с HTTP_PROXY/HTTPS_PROXY на alpha и пустым NO_PROXY дошёл до HTTP-origin через alpha.
  2. Тот же ребёнок для HTTPS получил 200. Метки X-Lab-Proxy на TLS-origin нет: после CONNECT прокси уже не вставляет HTTP-заголовки в origin. Маршрут здесь держит сам факт живого origin, плюс запись CONNECT в фикстуре.
  3. NO_PROXY с именем лабораторного хоста убрал метку шлюза: запрос стал прямым.
  4. trust_env=False при тех же переменных тоже ушёл напрямую.
  5. В среде торчал beta, в Client(proxy=...) передали alpha. Origin увидел alpha. Явный параметр победил среду, и это видно по идентификатору, не по статусу.
  6. SSL_CERT_FILE с лабораторным CA и trust_env=True открыл HTTPS без явного verify=.
  7. Тот же SSL_CERT_FILE при trust_env=False и verify=True упал: SSLCertVerificationError, issuer неизвестен. Флаг не выключает проверку сертификата. Он перестаёт читать CA из среды. Явный SSLContext по-прежнему проверяет цепочку.
Среда, обход и trust_env

На macOS urllib любит обходить «локальные» имена. Поэтому цель ходила на зарезервированный httpx-lab.test, а getaddrinfo в лаборатории подменял его на 127.0.0.1. Это инъекция резолвера, не замер внешней утечки DNS. Если убрать подмену и бить в 127.0.0.1, среда может тихо обойти прокси, и вы решите, что HTTP_PROXY «не работает».

Страница мой IP показывает маршрут браузера. Переменные процесса Python она не читает. Для резидентного пула смотрите каталог резидентских прокси, для мобильного мобильных. Тип IP и схема шлюза снова разные вопросы.

05Как закрывать sync, async и mounts

Синхронный клиент живёт в with httpx.Client(...). Асинхронный в async with httpx.AsyncClient(...), а вызовы ждут await. Документация отдельно предупреждает не плодить клиенты в горячем цикле: пул тогда бесполезен.

Лабораторный async-кейс был маленьким: AsyncClient с тем же proxy= и тем же SSL-контекстом, один GET. Он вернул 200 и метку alpha. Это не бенчмарк пула и не сравнение QPS.

Для mounts Transports просит отдельные HTTPTransport. Плейсхолдеры снова ваши: шлюз, цель и CA-файл, которым вы владеете.

import httpx
import ssl

PROXY_URL = "http://USER:PASSWORD@proxy.example:8080"
TARGET_URL = "https://origin.example/"
CA_FILE = "path/to/your-ca.pem"  # только если origin с частным CA

ctx = ssl.create_default_context(cafile=CA_FILE)
transport = httpx.HTTPTransport(
    proxy=PROXY_URL,
    verify=ctx,
    trust_env=False,
    retries=0,
)
try:
    with httpx.Client(mounts={"all://": transport}, trust_env=False) as client:
        r = client.get(TARGET_URL)
finally:
    transport.close()

Транспорт получил и прокси, и CA, и trust_env=False. Клиентский verify= сюда сам не протекает. HTTPS через эту схему вернул origin=https-origin. Если забыть CA на транспорте, получите ту же SSLCertVerificationError, хотя «прокси вроде задан».

retries=0 на транспорте ограничивает отказ соединения. Официально ретраи только на ConnectError/ConnectTimeout. Ответ 429, чтение тела и запись запроса этот счётчик не повторяет. Переносить urllib3.Retry.allowed_methods в HTTPX не стоит: это другой стек.

06Как отличить отказ, ReadTimeout и ошибку TLS

Тайм-ауты в HTTPX разные. Timeouts делит connect, read, write и pool. Read timeout это максимум ожидания очередного куска данных, в том числе статус-строки и заголовков, не только пауза внутри уже начатого тела. Дефолт простоя 5 секунд. Лабораторный гарнесс трогал connect и read. Write и pool в прогоне не вызывали, поэтому в таблице ниже они не «проверены», а только описаны.

Измеренный случай гарнесса это разрыв тела. /slow-body сразу отдал заголовки, сбросил первый байт H, выставил событие и только потом ждал дольше read=0.4. Пока клиент читал продолжение тела, поднялся httpx.ReadTimeout над httpcore.ReadTimeout и TimeoutError. Первый байт к тому моменту уже ушёл. Этот прогон доказывает паузу после первого байта. Он не закрывает ветку, где origin молчит до заголовков.

На той же установленной 0.28.1 отдельная loopback-проверка без отдачи статус-строки сняла ReadTimeout, пока заголовки ещё не ушли. Значит read timeout умеет истечь и в ожидании заголовков, и между кусками тела. Новый матричный прогон гарнесса для этого не запускали.

Отказ соединения собирали отдельно. Сокет, который на Darwin 24.6 только bind без listen, RST не шлёт: сырой connect ждал таймаут, HTTPX поднял ConnectTimeout. Называть это отказом нельзя. Настоящий ECONNREFUSED появился, когда тот же порт закрыли и сразу постучались: httpx.ConnectError → ConnectionRefusedError. Между закрытием и запросом есть крошечное окно повторного захвата порта. Это не «измерили connect timeout сети до 192.0.2.1».

Отказ, idle-bind и ReadTimeout

Проверка IP в браузере снова ни при чём. Лабораторный ReadTimeout не говорит, какой пинг будет у резидентного пула.

СимптомЧто увидели в 0.28.1 на Darwin 24.6Что это не доказывает
HTTP без пароля шлюзаОтвет 407, без исключенияПоведение облачного провайдера
HTTP с неверным паролемСнова 407Что CONNECT ведёт себя так же
HTTPS CONNECT без пароляhttpx.ProxyError / httpcore.ProxyErrorЕдиный текст ошибки на всех версиях
Недоверенный TLSConnectError + SSLCertVerificationErrorЧто «прокси мёртв»
trust_env=False при SSL_CERT_FILEТа же ошибка issuerЧто проверка CA выключена
Bind без listenConnectTimeoutОтказ соединения
Порт закрыт, сразу GETConnectError + ConnectionRefusedErrorСтабильный таймаут сети
/slow-body после первого байтаReadTimeout на чтении телаЧто read timeout не бывает до заголовков
SOCKS5 и SOCKS5h на lab-имяATYP DOMAIN, 200Где резолвится чужой DNS в проде
Сводка 25 случаев и 38 утверждений

Прогон: 2026-10-14T18:15:14Z–18:15:15Z UTC, CPython 3.12.12, httpx 0.28.1, httpcore 1.0.9, socksio 1.0.0. Команда: tests/.venv/bin/python tests/harness.py. Записано 25 случаев, 38 утверждений, 38 прошли, 0 нет. Это не 25 «успехов» из-за 25 объектов. У каждого ожидаемого результата было отдельное assert.

07Какие шлюзы подходят по документации

Лаборатория не ходила на платные входы и не гоняла SDK провайдеров. Ниже только то, что написано в официальных текстах на 2026-10-14, плюс публичные карточки каталога этой локали. Это совпадение протокола и схемы входа по документам, не тест аккаунта и не гарантия пропуска.

README резидентных прокси Oxylabs обещает HTTP, HTTPS и SOCKS5, вход по логину/паролю или по белому IP, шлюз http://pr.oxylabs.io:7777 и префикс customer-USERNAME. Карточка каталога: Oxylabs.

Quick start Decodo показывает gate.decodo.com:7000, логин/пароль или белый IP, и в примере Requests один и тот же http://...@gate.decodo.com:7000 для ключей http и https. Русская карточка по-прежнему smartproxy-com: исторический слаг Decodo, не новое имя компании.

Резидентный пул не равен «логин с мобильного IP». Если нужен мобильный выход, смотрите мобильные прокси. Боевой IP после настройки всё равно стоит сверить прокси-чекером, а не этой статьёй.

Oxylabs — резидентские и датацентровые прокси
Oxylabs — резидентские и датацентровые прокси4.0770

Oxylabs продаёт резидентские, датацентровые и мобильные прокси. Резидентские планы от $6/GB (5 GB / мес). Оплата: карты, банковский перевод, AliPay, PayPal.

10 стран
0,59 $
за 1 GB
Decodo (Smartproxy) — резидентские и датацентровые прокси
Decodo (Smartproxy) — резидентские и датацентровые прокси4.22107

Decodo (ранее Smartproxy) продаёт резидентские и датацентровые прокси. Резидентские: $4/GB без месячного плана, подписки от $3.75/GB за 3 GB. Оплата: карты, PayPal, Alipay, Google/Apple Pay, крипто (с оговорками).

83 страны
0,38 $
за 1 GB

Как задать прокси в HTTPX?

В 0.28.1 передайте один URL в proxy= у Client, AsyncClient или httpx.get. Словаря proxies= больше нет. Для разных шлюзов по схемам соберите mounts из HTTPTransport, и на каждом транспорте сами укажите proxy, TLS-контекст и trust_env.

Почему для HTTPS-цели адрес HTTP-прокси начинается с http://?

Клиент соединяется с шлюзом обычным HTTP и просит CONNECT. TLS начинается уже внутри туннеля, с origin. Схема http:// у прокси здесь про первый TCP, не про «сайт без шифра».

Чем авторизация прокси отличается от auth=?

Пароль шлюза идёт в userinfo либо в httpx.Proxy(auth=...) и становится Proxy-Authorization. Параметр auth= вешает Authorization на цель. В лаборатории origin видел целевой Basic и не видел проксиный.

Отключает ли trust_env=False проверку сертификатов?

Нет. Флаг перестаёт читать HTTP_PROXY и SSL_CERT_FILE. Явный SSLContext по-прежнему проверяет цепочку. В прогоне trust_env=False при заданном SSL_CERT_FILE дал SSLCertVerificationError, а тот же CA в create_default_context открыл HTTPS.

Почему ReadTimeout возникает после получения заголовков?

Read timeout в HTTPX 0.28.1 ждет очередной кусок данных, в том числе статус и заголовки. Лабораторный /slow-body как раз молчал уже после первого байта тела, поэтому ReadTimeout там на чтении остатка. Отдельная проверка той же 0.28.1 без отдачи заголовков тоже сняла ReadTimeout. Значит ветка до статус-строки существует, просто гарнесс её не измерял.

Повторяет ли HTTPTransport ответы 429 и 503?

Нет. retries в HTTPTransport касается ConnectError и ConnectTimeout. Документация прямо отправляет статусы вроде 503 к отдельным библиотекам. urllib3 Retry сюда не переносится.