wrong version number — так OpenSSL сообщает, что начал рукопожатие TLS, а первые байты ответа оказались не TLS. На практике это обычный HTTP: клиент отправил ClientHello на порт, который отвечает открытым текстом, и ответ вроде HTTP/1.1 400 Bad Request невозможно прочитать как запись TLS. Сертификат, версия протокола и шифры здесь ни при чём, поэтому --tlsv1.2, --insecure и verify=False ничего не меняют.
За прокси такое рукопожатие может происходить в двух местах, и большинство клиентов печатают для обоих один и тот же текст:
- Участок до прокси. В URL прокси указано
https://, поэтому клиент начинает TLS с самим прокси, но порт прокси говорит на обычном HTTP. - Участок до цели. В URL прокси указано
http://,CONNECTпроходит успешно, и клиент начинает TLS через туннель с целевым портом, который не говорит на TLS, напримерhttps://example.com:80/.
| Клиент | Случай 1: участок до прокси | Случай 2: участок до цели |
|---|---|---|
| curl 8.18.0 | curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number | Та же строка |
| Requests 2.34.2 | ProxyError: Unable to connect to proxy. Your proxy appears to only use HTTP and not HTTPS, try changing your proxy URL to be HTTP. | SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number, без подсказки |
| HTTPX 0.28.1 | ConnectError: [SSL: WRONG_VERSION_NUMBER] wrong version number | Та же строка |
Только Requests называет участок. Для curl и HTTPX нужна ещё одна проверка, она описана ниже. Строки Python получены на Python 3.12.3 с OpenSSL 3.0.13; более новые сборки OpenSSL формулируют тот же сбой иначе, и в последней таблице на этой странице собраны все записанные варианты.
Как отличить один участок от другого
Сначала прочитайте URL прокси. Для случая 1 нужен URL прокси с https://. Выведите то, что процесс использует на самом деле, включая переменные, которые задавали не вы:
env | grep -i _proxyЕсли все URL прокси уже начинаются с http://, у вас случай 2.
curl: смотрите статус CONNECT. %{http_connect} — это статус ответа прокси на CONNECT. Ноль означает, что curl до этого шага не дошёл.
curl --silent --show-error --output /dev/null \
--proxy "$PROXY_URL" \
--write-out 'connect=%{http_connect} exit=%{exitcode}\n' \
https://example.com/connect=000 exit=35: не удалось рукопожатие с прокси. Случай 1.connect=200 exit=35: туннель открылся, а рукопожатие с целью не удалось. Случай 2.
curl -v показывает то же самое. В случае 2 эти строки идут перед ошибкой; в случае 1 строки CONNECT нет вовсе:
> CONNECT 127.0.0.1:18000 HTTP/1.1
< HTTP/1.1 200 Connection established
* CONNECT tunnel established, response 200
* TLS connect error: error:0A00010B:SSL routines::wrong version numberПодробный вывод печатает и заголовок Proxy-Authorization, поэтому делитесь строкой write-out, а не трассировкой.
Requests: смотрите класс исключения. requests.exceptions.ProxyError означает, что не удалось соединение с прокси. requests.exceptions.SSLError означает, что TLS не удался уже после открытия туннеля.
import requests
proxy_url = "http://USERNAME:PASSWORD@proxy.example.net:8080"
proxies = {"http": proxy_url, "https": proxy_url}
try:
requests.get("https://example.com/", proxies=proxies, timeout=(10, 20))
except requests.exceptions.ProxyError as error:
print("proxy hop:", type(error).__name__)
except requests.exceptions.SSLError as error:
print("target hop:", type(error).__name__)Подсказку добавляет urllib3, и только когда схема URL прокси — https, а ошибка TLS содержит wrong version number, unknown protocol или record layer failure. Старые версии были менее аккуратны: urllib3 1.26.8 печатал подсказку и для URL прокси с http:// (issue #2564). На старом urllib3 доверяйте выведенному URL прокси, а не подсказке.
HTTPX: смените схему или прочитайте журнал httpcore. HTTPX в обоих случаях выбрасывает один и тот же ConnectError. Если URL прокси начинается с https://, замените его на http:// и запустите снова: если ошибка исчезла, это был случай 1, а если осталась та же ошибка, это случай 2. Чтобы получить прямой ответ, включите отладочный журнал:
import logging
logging.basicConfig(level=logging.DEBUG)В случае 2 в журнале виден CONNECT с ответом 200, а затем сбой в httpcore.proxy:
DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'Connection established', [])
DEBUG:httpcore.proxy:start_tls.failed exception=ConnectError(SSLError(1, '[SSL: RECORD_LAYER_FAILURE] record layer failure (_ssl.c:1081)'))В случае 1 записи о CONNECT нет, а сбойная строка — httpcore.connection:start_tls.failed. Этот журнал снят на Python 3.14.4 с OpenSSL 3.5.5, поэтому в нём RECORD_LAYER_FAILURE.
Исправление случая 1: http:// в URL прокси
Схема в URL прокси описывает, как клиент разговаривает с прокси, а не что он через него запрашивает. URL прокси с http:// всё равно передаёт HTTPS-трафик: клиент отправляет CONNECT открытым текстом, а затем выполняет TLS с целью внутри туннеля. Используйте https://, только если провайдер документирует порт прокси с TLS. В документации curl к --proxy отсутствие схемы или http:// означает HTTP-прокси, а https:// — HTTPS-прокси.
curl --proxy http://USERNAME:PASSWORD@proxy.example.net:8080 https://example.com/import requests
proxy_url = "http://USERNAME:PASSWORD@proxy.example.net:8080"
response = requests.get(
"https://example.com/",
proxies={"http": proxy_url, "https": proxy_url},
timeout=(10, 20),
)import httpx
with httpx.Client(proxy="http://USERNAME:PASSWORD@proxy.example.net:8080") as client:
response = client.get("https://example.com/")export HTTPS_PROXY=http://USERNAME:PASSWORD@proxy.example.net:8080
export HTTP_PROXY=http://USERNAME:PASSWORD@proxy.example.net:8080В лаборатории порт прокси, который отказывал с https://, вернул 200 во всех трёх клиентах, как только схема стала http://, заданная в коде или через HTTPS_PROXY. Если ошибка осталась после замены, старое значение может по-прежнему приходить из переменной окружения: и Requests, и HTTPX по умолчанию читают HTTPS_PROXY (trust_env). В руководстве Переменные окружения прокси разобрано, какую переменную читает каждый клиент, а полные настройки есть в руководствах по curl, Requests и HTTPX.
Ловушка: ключ словаря — это не схема прокси
В proxies={"https": ...} ключ — это схема URL, который вы запрашиваете. Значение — это URL прокси со своей собственной схемой. Они не обязаны совпадать, а для порта прокси с обычным HTTP и не должны:
# Case 1: TLS to a proxy port that speaks plain HTTP
proxies = {"https": "https://USERNAME:PASSWORD@proxy.example.net:8080"}
# Correct: HTTPS targets through an HTTP proxy
proxies = {"https": "http://USERNAME:PASSWORD@proxy.example.net:8080"}С переменными окружения то же самое. Имя HTTPS_PROXY выбирает, какие запросы идут через прокси, а значение говорит, как до него добраться.
Исправление случая 2: исправьте URL цели
С прокси всё в порядке. URL просит TLS на порту, который отдаёт обычный HTTP. Типичные причины: https:// в сочетании с портом 80 или внутренним HTTP-портом, а также базовый URL, собранный из схемы и порта, которые настраиваются отдельно. Проверьте это, отправив обычный HTTP через тот же туннель:
curl --silent --show-error --proxy "$PROXY_URL" --proxytunnel \
--write-out 'connect=%{http_connect} status=%{http_code}\n' \
http://example.com:80/В лаборатории для порта, который отказывал с https://, эта команда вывела страницу и connect=200 status=200, а порт, который действительно говорит на TLS, дал curl: (52) Empty reply from server. HTTP-статус здесь означает, что порт говорит на обычном HTTP: смените URL на http:// или направьте его на порт с TLS. Если вам разрешено подключаться без прокси, тот же URL точно так же падает напрямую, и это заодно снимает подозрение с прокси.
Если URL верный, а ошибка то появляется, то исчезает, значит, байты, приходящие через туннель, идут не от TLS-сервера вашей цели. В issue #2564 urllib3 описана такая картина с нестабильным прокси. Запишите строку connect= с отметкой времени и отправьте её провайдеру.
Повторные попытки не помогут ни в одном случае
Оба случая — ошибки конфигурации, и каждая попытка падает одинаково. Max retries exceeded в сообщении Requests не означает, что повторы выполнялись: адаптер по умолчанию настроен на ноль повторов и всё равно печатает этот текст. Политика повторов лишь откладывает то же исключение. Исправьте URL, а повторы оставьте для сбоев, которые могут измениться от попытки к попытке.
В совместимом с MCP агенте для программирования бесплатный Proxy Toolkit MCP диагностирует ошибки прокси по фазе запроса. Сообщите ему клиент, исключение и значение connect=, без учётных данных.
Формулировка зависит от сборки OpenSSL
Текст приходит из библиотеки TLS, а не из клиента, поэтому один и тот же сбой в разных сборках выглядит по-разному. Каждая сборка печатала одну и ту же строку на обоих участках, если не указано иное:
| Сборка | Текст ошибки |
|---|---|
| curl 8.18.0 (OpenSSL 3.5.5), curl 8.17.0 (OpenSSL 3.6.0), curl 8.22.0 (OpenSSL 4.0.2) | curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version number |
| curl 8.12.1 (OpenSSL 3.4.1) | curl: (35) TLS connect error: error:0A0000C6:SSL routines::packet length too long |
| curl 8.7.1 (OpenSSL 3.2.1) | curl: (35) OpenSSL/3.2.1: error:0A0000C6:SSL routines::packet length too long |
| Python 3.11.3 (OpenSSL 1.1.1s), Python 3.12.3 (OpenSSL 3.0.13) | [SSL: WRONG_VERSION_NUMBER] wrong version number |
| Python 3.14.4 (OpenSSL 3.5.5) | [SSL: RECORD_LAYER_FAILURE] record layer failure |
Node.js 24.20.0 fetch (OpenSSL 3.5.7) | TypeError: fetch failed, код причины ERR_SSL_WRONG_VERSION_NUMBER |
| Playwright 1.63.0 (Chromium 153) | Случай 1: net::ERR_PROXY_CONNECTION_FAILED. Случай 2: net::ERR_SSL_PROTOCOL_ERROR |
Значит, packet length too long и record layer failure при работе через прокси — это те же два случая. На всех трёх сборках Python Requests выбрасывал ProxyError с подсказкой в случае 1 и просто SSLError в случае 2. unknown protocol, третья фраза, которую ищет urllib3, не появилась ни на одной из протестированных сборок. Chromium здесь единственный клиент, который сам различает участки; его ошибки прокси разобраны в руководстве Исправляем ERR_TUNNEL_CONNECTION_FAILED, а чтение цепочки cause — в руководстве Node.js fetch failed за прокси.
Как это тестировалось
4 октября 2026 года, на VPS с Ubuntu 26.04, всё на 127.0.0.1: слушатель, который на любой ввод отвечает открытым текстом HTTP/1.1 400 Bad Request, минимальный CONNECT-прокси на обычном HTTP, python -m http.server как цель с обычным HTTP и тот же обработчик за одноразовым сертификатом как рабочая цель с TLS. Без аккаунта у прокси-провайдера и без исходящего трафика. В случае 1 URL прокси с https:// указывал на слушатель с открытым текстом и на CONNECT-прокси. В случае 2 использовался CONNECT-прокси и URL с https:// для порта с обычным HTTP. Контрольный прогон с URL прокси http:// и целью с TLS вернул 200 во всех клиентах.
Клиенты: перечисленные выше сборки curl; Requests 2.34.2 с urllib3 2.8.0 и HTTPX 0.28.1 с httpcore 1.0.9 на трёх сборках Python; fetch в Node.js 24.20.0 с NODE_USE_ENV_PROXY=1; Playwright 1.63.0 с Chromium 153.0.8010.12. Архив лаборатории содержит скрипт лаборатории, клиентские скрипты, README и записанные результаты.
Не тестировалось: macOS и Windows, библиотеки TLS, кроме OpenSSL и собственной библиотеки Chromium, SOCKS-прокси, настоящие порты прокси с TLS и коммерческие прокси-сети. Порт прокси, который закрывает соединение или молчит, не отвечая открытым текстом, тоже не тестировался и может дать другую ошибку.
Похожие руководства
- Переменные окружения прокси: HTTP_PROXY и NO_PROXY — какую переменную читают curl, Requests и Node.
- Прокси в curl —
-x, туннельCONNECTи коды выхода curl. - Прокси в Python Requests — словарь
proxiesиtrust_env. - Асинхронные прокси в HTTPX — явные клиенты и тайм-ауты пула.
- Исправляем ERR_TUNNEL_CONNECTION_FAILED — отказы прокси в Playwright и Puppeteer.
ipvolt — прокси-сервис для разработчиков, который пока не открыт; запишитесь в список раннего доступа, и вы получите одно письмо, когда он откроется.
Источники и дополнительное чтение
Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.
- urllib3: Your proxy appears to only use HTTP and not HTTPS
- urllib3 issue #2564: the HTTP-only proxy hint shown for http:// proxy URLs
- urllib3 2.8.0 source: _wrap_proxy_error in connection.py
- curl: -x, --proxy and the proxy URL scheme
- curl: --write-out variables including http_connect
- Requests: proxies
- HTTPX: proxies
- HTTPX: logging
- RFC 9110: the CONNECT method