Диагностика8 мин чтения

SSL WRONG_VERSION_NUMBER через прокси: где именно сбой

Ошибка SSL wrong version number через прокси бывает на двух участках: до прокси и до целевого порта. Как отличить их в curl, Requests и HTTPX и что исправить.

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

wrong version number — так OpenSSL сообщает, что начал рукопожатие TLS, а первые байты ответа оказались не TLS. На практике это обычный HTTP: клиент отправил ClientHello на порт, который отвечает открытым текстом, и ответ вроде HTTP/1.1 400 Bad Request невозможно прочитать как запись TLS. Сертификат, версия протокола и шифры здесь ни при чём, поэтому --tlsv1.2, --insecure и verify=False ничего не меняют.

За прокси такое рукопожатие может происходить в двух местах, и большинство клиентов печатают для обоих один и тот же текст:

  1. Участок до прокси. В URL прокси указано https://, поэтому клиент начинает TLS с самим прокси, но порт прокси говорит на обычном HTTP.
  2. Участок до цели. В URL прокси указано http://, CONNECT проходит успешно, и клиент начинает TLS через туннель с целевым портом, который не говорит на TLS, например https://example.com:80/.
КлиентСлучай 1: участок до проксиСлучай 2: участок до цели
curl 8.18.0curl: (35) TLS connect error: error:0A00010B:SSL routines::wrong version numberТа же строка
Requests 2.34.2ProxyError: 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.1ConnectError: [SSL: WRONG_VERSION_NUMBER] wrong version numberТа же строка

Только Requests называет участок. Для curl и HTTPX нужна ещё одна проверка, она описана ниже. Строки Python получены на Python 3.12.3 с OpenSSL 3.0.13; более новые сборки OpenSSL формулируют тот же сбой иначе, и в последней таблице на этой странице собраны все записанные варианты.

Как отличить один участок от другого

Сначала прочитайте URL прокси. Для случая 1 нужен URL прокси с https://. Выведите то, что процесс использует на самом деле, включая переменные, которые задавали не вы:

sh
env | grep -i _proxy

Если все URL прокси уже начинаются с http://, у вас случай 2.

curl: смотрите статус CONNECT. %{http_connect} — это статус ответа прокси на CONNECT. Ноль означает, что curl до этого шага не дошёл.

sh
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 нет вовсе:

code
> 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 не удался уже после открытия туннеля.

python
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. Чтобы получить прямой ответ, включите отладочный журнал:

python
import logging

logging.basicConfig(level=logging.DEBUG)

В случае 2 в журнале виден CONNECT с ответом 200, а затем сбой в httpcore.proxy:

code
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-прокси.

sh
curl --proxy http://USERNAME:PASSWORD@proxy.example.net:8080 https://example.com/
python
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),
)
python
import httpx

with httpx.Client(proxy="http://USERNAME:PASSWORD@proxy.example.net:8080") as client:
    response = client.get("https://example.com/")
sh
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 и не должны:

python
# 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 через тот же туннель:

sh
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 и коммерческие прокси-сети. Порт прокси, который закрывает соединение или молчит, не отвечая открытым текстом, тоже не тестировался и может дать другую ошибку.

Похожие руководства

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

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

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