Прокси в HTTPX: авторизация, переменные окружения и тайм-ауты
Как задать 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)
- Поставьте
httpx==0.28.1. В этой сборкеinspect.signature(httpx.Client.__init__)содержитproxyи не содержитproxies. - Подставьте
PROXY_URLиTARGET_URLсвоими значениями. Логин спецсимволами кодируйте черезquote(..., safe=''). - Держите клиент в
with. Иначе пул и транспорт живут дольше, чем вы думаете. - Смотрите не только статус 200. Прямой запрос к origin тоже даёт 200. В лаборатории origin писал заголовок
X-Lab-Proxy, и только явныйproxy=оставлял тамalpha.

Прямой 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.
- Возьмите HTTP-прокси, даже если цель
https://. - Для HTTPS к своему origin с частным CA соберите контекст из файла, которым вы владеете:
ssl.create_default_context(cafile=CA_FILE)и передайте его вverify=. Не ставьтеverify=False. - Убедитесь, что шлюз умеет туннель, а не отвечает заглушкой.
- Проверьте, что
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.
- Соберите URL шлюза без логина, если хотите отдать пару в
httpx.Proxy.auth. Конструктор 0.28.1 принимаетauthкортежем. - Прогоните три случая отдельно: нет пароля, неверный, верный. Не склеивайте их одним «если 407».
- Для HTTP-форварда наша фикстура ответила обычным 407 и на пустой, и на неверный Basic.
- Для HTTPS CONNECT те же фикстурные 407 HTTPX поднял как
httpx.ProxyErrorнадhttpcore.ProxyError. Это не тот же объект, что статус 407 у форварда. - Отдельным запросом передайте
auth=цели. Origin увиделAuthorizationи не увиделProxy-Authorization.

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

На 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».

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

Прогон: 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 после настройки всё равно стоит сверить прокси-чекером, а не этой статьёй.





