Tunnel connection failed: 502/503 через прокси: причины и решения

Ошибка 502, 503 или 504 при CONNECT приходит от прокси, а 502 после открытия туннеля — от сайта. Как различить их в каждом клиенте и что исправлять.

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

502 Bad Gateway, 503 Service Unavailable или 504 Gateway Timeout, пришедший в тот момент, когда клиент открывает туннель через прокси, отправил прокси: туннель так и не открылся, и сайт вашего запроса не видел. 502, пришедший после открытия туннеля, отправил сайт или то, что стоит перед ним. Код тот же, источник другой, и исправление другое. Каждая строка ошибки и каждое число ниже получены на loopback-стенде 5 ноября 2026 года; ссылка на него — в конце.

Кто отправил 502

Для URL https:// клиент сначала просит прокси открыть туннель запросом CONNECT host:443 (RFC 9110). Если прокси отвечает чем-либо, кроме 2xx, туннеля нет, и большинство клиентов выбрасывают исключение. Если туннель открылся, а сайт ответил 502, вы получаете обычный ответ.

КлиентПрокси отказал в туннелеСайт ответил 502
curl 8.5.0код выхода 56, curl: (56) CONNECT tunnel failed, response 502код выхода 0, %{http_code} равен 502, %{http_connect} равен 200
curl 8.18.0код выхода 7, curl: (7) CONNECT tunnel failed, response 502код выхода 0, %{http_code} равен 502, %{http_connect} равен 200
Requests 2.34requests.exceptions.ProxyError с текстом Tunnel connection failed: 502 Bad GatewayResponse с status_code == 502
HTTPX 0.28httpx.ProxyError: 502 Bad GatewayResponse с status_code == 502
aiohttp 3.14ClientHttpProxyError: 502, message='Bad Gateway'ответ со status == 502
Playwright 1.63page.goto: net::ERR_TUNNEL_CONNECTION_FAILEDpage.goto завершается успешно, status() равен 502
Node.js 24 fetchTypeError: fetch failed, а err.cause.cause.message равен Proxy response (502) !== 200 when HTTP Tunnelingresponse.status равен 502

503 и 504 ведут себя так же, только со своим номером, например Tunnel connection failed: 503 Service Unavailable или curl: (7) CONNECT tunnel failed, response 504. Chromium во всех трёх случаях сообщил net::ERR_TUNNEL_CONNECTION_FAILED без статуса; это семейство ошибок разобрано в руководстве по ERR_TUNNEL_CONNECTION_FAILED.

Requests пишет Max retries exceeded, хотя по умолчанию ничего не повторяет: прокси на стенде получал ровно один CONNECT на запрос. Node прячет статус на два уровня глубже, поэтому логируйте всю цепочку cause.

Из этого правила есть два исключения. Для обычного URL http:// туннеля нет, поэтому curl, Requests, HTTPX, aiohttp и Playwright возвращают собственный 502 прокси как обычный ответ; сравните его заголовок Server и тело с тем, что обычно отдаёт сайт. Fetch в Node туннелирует и URL http://, так что там это остаётся исключением. А прокси, который закрывает соединение без статуса, вообще не даёт номера: curl вывел curl: (56) Proxy CONNECT aborted, Requests выбросил ConnectionError с RemoteDisconnected('Remote end closed connection without response'), HTTPX — RemoteProtocolError: Server disconnected without sending a response., а Chromium — net::ERR_EMPTY_RESPONSE.

Проверка одним запросом curl

Выведите оба кода статуса для проблемного URL через тот же прокси:

sh
curl --disable -sS -o /dev/null -x "$PROXY_URL" \
  -w 'connect=%{http_connect} http=%{http_code} exit=%{exitcode}\n' \
  https://example.com/
  • connect=502 http=000: прокси отказал в туннеле; смотрите причины ниже.
  • connect=200 http=502: 502 ответил сайт; смотрите раздел если 502 отправил сайт.
  • connect=000 и Proxy CONNECT aborted: прокси закрыл соединение без статуса. Сначала проверьте порт.

Причины на стороне прокси

Сайт недоступен с выходного узла (502). Прокси не смог подключиться дальше; на стенде отклонённое соединение превратилось в 502 Bad Gateway во всех клиентах. Запросите тот же URL без прокси и через другой выходной узел или страну. Если не работает только один узел, сайт оттуда недоступен или блокирует эту сеть, поэтому смените узел. Если не работает ни один, проверьте хост и порт в строке CONNECT.

Выходной узел офлайн или меняет IP (503, иногда 502). Шлюзу некуда отправить запрос. Повторяйте идемпотентные запросы с паузами, как показано ниже, и проверьте состояние узла в панели провайдера.

Прокси не дождался (504). Истёк собственный срок, который прокси даёт себе на подключение к сайту. Прокси на стенде ждал 3 секунды, и все клиенты получили ошибку через 3,0–3,5 секунды. Таймаут клиента должен быть больше этого срока, иначе вместо 504 вы увидите собственный таймаут; руководство по таймаутам прокси помогает их различить.

Не тот порт или протокол. Мы направили все клиенты, как на HTTP-прокси, на порт, который понимает только SOCKS5 (pproxy 2.7.9). Сервер закрыл соединение без ответа, так что никакого 502 не было: curl 8.5.0 и 8.18.0 вывели curl: (56) Proxy CONNECT aborted, aiohttp выбросил ServerDisconnectedError, а fetch в Node не завершился, пока не сработал его 10-секундный таймаут. Копируйте схему, хост и порт со страницы настройки у провайдера. URL прокси https:// на обычном HTTP-порту ломается по-другому; смотрите SSL WRONG_VERSION_NUMBER за прокси.

Нехватка мощности у провайдера. Перегруженный шлюз может отвечать 502 или 503 на новые туннели. Снизьте параллельность, сделайте паузу и отправьте провайдеру время сбоев в UTC.

На том же шаге 407 означает проблему аутентификации (как исправить ошибку прокси 407), а 403 — намеренный отказ (CONNECT tunnel failed, response 403).

Мобильные прокси и смена IP

В мобильном прокси выходной узел — это устройство в сети оператора связи. Пока оно меняет IP, рабочего выхода может на мгновение не быть; в зависимости от шлюза клиент тогда видит 502 или 503 на CONNECT либо закрытое соединение и сообщает о них так, как описано выше. Реальную смену IP мы не измеряли, поэтому длительностей в этом руководстве нет. Если вы запускаете смену сами, приостановите запросы, пока не пройдёт дешёвый тестовый запрос.

Стенд имитировал выходной узел, недоступный 4 секунды, и отправлял один запрос Requests в секунду. Секунды с 0 по 3 завершились ошибкой Tunnel connection failed: 503 Service Unavailable; начиная с 4-й секунды каждый запрос возвращал 200. 4 секунды — настройка стенда: опыт показывает форму смены IP, а не её длительность.

Встроенные повторы обращаются с двумя сторонами по-разному. curl --retry 2 отправил CONNECT на сбойный прокси один раз и остановился, но трижды запросил у сайта его 502; с --retry-all-errors он повторял и CONNECT. Retry(total=2, status_forcelist=[502, 503, 504]) из urllib3 повторял и то и другое. Fetch в Node ничего не повторял после статуса, но когда прокси закрывал соединение без статуса, переподключился 160 357 раз за 10 секунд, пока его не остановил AbortSignal.timeout. Всегда передавайте signal.

Повторяйте только GET-запросы. Неудавшийся CONNECT до сайта не дошёл, но 502 от сайта может прийти уже после того, как сайт выполнил работу, и повторённый POST выполнится дважды; см. один потерянный ответ — два созданных задания. Эта функция повторяет GET после 502, 503 или 504 от прокси или сайта с ограниченными паузами со случайным разбросом и останавливается на всём остальном, например на 407:

python
import random, re, time
import requests

RETRYABLE = {502, 503, 504}
TUNNEL_STATUS = re.compile(r"Tunnel connection failed: (\d{3})")

def get_with_retry(session, url, attempts=5, base_delay=1.0, max_delay=8.0, **kwargs):
    for attempt in range(1, attempts + 1):
        try:
            response = session.get(url, **kwargs)
        except requests.exceptions.ProxyError as exc:
            match = TUNNEL_STATUS.search(str(exc))  # Requests reports the CONNECT status only as text
            if match is None or int(match.group(1)) not in RETRYABLE or attempt == attempts:
                raise
        else:
            if response.status_code not in RETRYABLE or attempt == attempts:
                return response
        time.sleep(min(max_delay, base_delay * 2 ** (attempt - 1)) * random.uniform(0.5, 1.0))

В записанном прогоне она пережила 4-секундное окно недоступности на четвёртой попытке, через 5,7 секунды. С сайтом, который продолжал отвечать 502, она сдалась после пяти попыток и 9,9 секунды и вернула 502.

Если 502 отправил сайт

Если туннель открылся, прокси свою работу сделал: 502 пришёл от шлюза, балансировщика нагрузки или CDN сайта, которые не получили пригодного ответа от приложения за ними. Смена прокси или IP этого не исправит. Запросите URL без прокси; если и так не работает, подождите и повторяйте GET с помощью функции выше. Если ошибка возникает только через прокси, сайт может отдавать ошибки этой сети; сравните тело и заголовки с прямым запросом, прежде чем менять узел.

Как воспроизвести стенд

В папке загрузки лежат lab.mjs (прокси на 127.0.0.1, который отвечает на CONNECT кодом 502, 503 или 504 либо обрывает соединение, и TLS-сервер, который сам отвечает 502), по одному скрипту на клиент, wrong_port.mjs, node_checks.mjs и retry_get.py с функцией выше. В results.json записаны версии: curl 8.5.0 и 8.18.0, Python 3.14.4, Requests 2.34.2, urllib3 2.8.0, HTTPX 0.28.1, aiohttp 3.14.5, Node.js 24.22.1 с undici 7.30.1 и Playwright 1.63.0 с Chromium 153. Не проверялись: macOS и Windows, SOCKS- и HTTPS-прокси и реальные шлюзы провайдеров.

В агенте для программирования Proxy Toolkit MCP помогает диагностировать ошибки; передайте ему клиент и очищенный текст ошибки, но никогда не учётные данные.

Мобильные прокси ipvolt — это выделенные устройства в сетях операторов; вы меняете IP по запросу или по расписанию, так что смена IP не застанет вас врасплох.

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

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