net::ERR_TUNNEL_CONNECTION_FAILED означает, что Chromium дошёл до вашего прокси, запросом CONNECT попросил его открыть туннель к HTTPS-назначению, и прокси ответил чем-то, кроме успеха. net::ERR_PROXY_CONNECTION_FAILED означает, что Chromium до этого не дошёл: он вообще не смог установить соединение с прокси. Оба кода приходят из сетевого стека Chromium, поэтому Playwright, Puppeteer и любой другой инструмент, управляющий Chromium, сообщают те же строки, обычно в виде page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://....
Самое досадное — то, что Chromium опускает. Прокси, который отказывает в туннеле с 403, прокси, который ограничивает вам частоту запросов с 429, и прокси, у которого не работает upstream с 502, — все дают одинаковый ERR_TUNNEL_CONNECTION_FAILED. Это руководство показывает, какое поведение прокси даёт какую ошибку в Playwright, Puppeteer и curl, — воспроизведённую на небольших контролируемых прокси, а не собранную по форумным темам, — а затем даёт короткую процедуру восстановления статуса, который браузер скрывает.
Видите это в Chrome или Edge без всякого кода?
В обычном окне браузера смысл тот же. Chrome и Edge используют сетевой стек Chromium и по умолчанию берут настройки прокси из операционной системы, поэтому ошибка говорит о том, что где-то настроен прокси и он отказался открывать туннель (ERR_TUNNEL_CONNECTION_FAILED) либо оказался вовсе недоступен (ERR_PROXY_CONNECTION_FAILED). Сам сайт не лежит; браузер просто не прошёл дальше прокси. Обычные источники такой конфигурации — VPN-клиент, который на время подключения устанавливает системный прокси, браузерное расширение прокси или VPN, корпоративная политика или PAC-скрипт на управляемом устройстве, либо прокси, введённый вручную и забытый.
Как это исправить, в порядке, который быстрее всего находит причину:
- Откройте
chrome://net-internals/#proxy(илиedge://net-internals/#proxy), чтобы увидеть действующие настройки прокси, которые браузер использует прямо сейчас. Если вы не ожидали там увидеть прокси, в этом и проблема. - Отключите VPN или расширения прокси, перезагрузите страницу и сравните. Если страница загрузилась, прокси VPN или расширения отказывал в туннеле или был недоступен.
- На рабочем или учебном устройстве правила администратора прокси определяют, какие сайты разрешены.
ERR_TUNNEL_CONNECTION_FAILEDна HTTPS-сайте, заблокированном политикой, — ожидаемое поведение, и изменить его может только администратор. - Если страницы
http://загружаются, аhttps://— нет, прокси отказывает вCONNECTк порту 443. Это случай туннеля из матрицы ниже, и причину называет собственный код статуса прокси.
Остальная часть руководства написана для разработчиков, которые сами настроили прокси в Playwright или Puppeteer, но каждая строка ошибки означает ровно то же самое и в адресной строке.
Две ошибки, два этапа
Для HTTPS-назначения через HTTP-прокси Chromium разрешает имя прокси, открывает к нему TCP-соединение, отправляет CONNECT host:443, ждёт ответ 2xx и только затем начинает TLS-рукопожатие с целевым сервером внутри туннеля. Два кода ошибок отмечают две разные точки этого пути:
ERR_PROXY_CONNECTION_FAILED: имя прокси не разрешилось, никто не принял TCP-соединение или соединение с прокси оборвалось до обмена какими-либо HTTP-данными. Частый вариант, который создают себе сами: написатьhttps://перед шлюзом, который говорит только на обычном HTTP, — тогда Chromium пытается выполнить TLS-рукопожатие с сервером, ожидающим открытый текст.ERR_TUNNEL_CONNECTION_FAILED: TCP-соединение установлено и прокси ответил наCONNECT, но не успешным статусом. Chromium отбрасывает код статуса, фразу-причину и любое тело, которое отправил прокси.
Всё остальное, что вы можете увидеть (ERR_CONNECTION_RESET, ERR_INVALID_HTTP_RESPONSE, ERR_SOCKS_CONNECTION_FAILED, обычный таймаут навигации), относится к другому этапу и рассматривается ниже.
Как выглядит каждый отказ прокси
В таблице записано, что сообщил каждый клиент при одном намеренно заданном поведении прокси. Playwright использовал опцию запуска proxy; Puppeteer — сырые флаги Chromium (--proxy-server) с page.authenticate для учётных данных; curl — -x с --proxy-user. Все три работали с одной и той же сборкой Chromium 153 или curl 8.18 под Linux; подробности в разделе о методике.
| Поведение прокси | Playwright 1.63 (Chromium 153) | Puppeteer 25 (тот же Chromium) | curl 8.18 |
|---|---|---|---|
| Имя прокси не разрешается | ERR_PROXY_CONNECTION_FAILED | то же | (5) Could not resolve proxy |
| На порту прокси никто не слушает | ERR_PROXY_CONNECTION_FAILED | то же | (7) Failed to connect |
Схема https://, но прокси говорит на обычном HTTP | ERR_PROXY_CONNECTION_FAILED | то же | (35) TLS connect error |
| Прокси принимает TCP и никогда не отвечает | таймаут навигации | таймаут навигации | (28) Connection timed out |
| Прокси принимает TCP и сразу закрывает соединение | ERR_CONNECTION_RESET | то же | (56) Proxy CONNECT aborted или (56) Recv failure: Connection reset by peer |
| Порт отвечает, но не по HTTP | ERR_INVALID_HTTP_RESPONSE | то же | (56) Proxy CONNECT aborted |
| На CONNECT ответ 403 | ERR_TUNNEL_CONNECTION_FAILED | то же | (7) CONNECT tunnel failed, response 403 |
| На CONNECT ответ 429 | ERR_TUNNEL_CONNECTION_FAILED | то же | (7) CONNECT tunnel failed, response 429 |
| На CONNECT ответ 502 или 503 | ERR_TUNNEL_CONNECTION_FAILED | то же | (7) CONNECT tunnel failed, response 502 или 503 |
| На CONNECT ответ 407, учётные данные не заданы | навигация завершается ответом со статусом 407, затем ERR_TUNNEL_CONNECTION_FAILED на запросе | ERR_INVALID_AUTH_CREDENTIALS | (7) CONNECT tunnel failed, response 407 |
| На CONNECT ответ 407, неверные учётные данные | навигация завершается ответом со статусом 407, затем ERR_TUNNEL_CONNECTION_FAILED на запросе | навигация завершается ответом со статусом 407 и страницей ошибки Chromium, затем ERR_HTTP_RESPONSE_CODE_FAILURE | (7) CONNECT tunnel failed, response 407 |
| Верные учётные данные | 200 от целевого сервера | 200 | 200, http_connect 200 |
| На CONNECT ответ 200, затем прокси обрывает туннель | ERR_CONNECTION_CLOSED или ERR_CONNECTION_RESET | ERR_CONNECTION_RESET | (35) Send failure: Broken pipe |
Схема socks5:// против HTTP-прокси | ERR_SOCKS_CONNECTION_FAILED | то же | (97) Received invalid version in initial SOCKS5 response |
socks5:// с именем пользователя и паролем | отказ при запуске: Browser does not support socks5 proxy authentication | не проверялось | не проверялось |
Обычное http:// назначение, прокси отвечает 403 или 407 | ошибки нет: собственная страница 403 или 407 прокси становится ответом навигации | то же | 403 или 407 с http_connect 000 |
Три строки заслуживают отдельного внимания. Любой неуспешный статус CONNECT от 403 до 503 схлопывается в одну ошибку браузера, поэтому по одному лишь браузеру нельзя понять, заблокированы ли вы, ограничены по частоте запросов или имеете дело с неработающим upstream. 407 — единственный статус, который Chromium обрабатывает особым образом, и Playwright с Puppeteer показывают его по-разному. А обычное http:// назначение никогда не даёт ошибку туннеля, потому что туннеля нет: отказ прокси приходит как обычный ответ со статусом и телом прокси, из-за чего один и тот же скрипт может «работать» на HTTP-тестовой странице и падать на нужном вам HTTPS-сайте.
Восстановите статус CONNECT с помощью curl
curl сохраняет статус, который Chromium выбрасывает. Выполните один ограниченный запрос к тому же шлюзу, целевому серверу и с теми же учётными данными, что и ваш браузерный скрипт, и выведите и статус целевого сервера, и статус CONNECT. Команда читает PROXY_URL, PROXY_USERNAME и PROXY_PASSWORD из окружения, поэтому пароль никогда не попадает в аргумент shell или в историю команд; для подстановки переменных требуется curl 8.3 или новее.
: "${PROXY_URL:?Set the gateway as http://host:port (no credentials in the URL)}"
CHECK_URL=${CHECK_URL:-https://example.com/}
curl --disable --silent --show-error --output /dev/null \
--noproxy '' --proxy "$PROXY_URL" --proxy-basic \
--variable %PROXY_USERNAME --variable %PROXY_PASSWORD \
--expand-proxy-user '{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}' \
--max-time 15 \
--write-out 'destination=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' \
"$CHECK_URL"connect=200 вместе со статусом целевого сервера означает, что туннель работает и проблема браузера в чём-то другом. Ненулевое значение connect — это статус, который скрыл Chromium:
- 407: прокси хочет учётные данные, которые он не принял. Проверьте аккаунт или субпользователя, percent-encoding и то, аутентифицирует ли этот шлюз по паролю или по разрешённому исходному IP-адресу. Руководство по ошибке 407 проходит этот порядок по шагам.
- 403: прокси принял соединение, но отказывает именно в этом туннеле. Типичные причины — целевой сервер или порт вне разрешённого провайдером списка, ограничение аккаунта либо параметр страны или сессии, который шлюз отвергает.
- 429: ограничения параллелизма или частоты запросов на прокси. Поищите заголовок
Retry-Afterв выводеcurl -vи уменьшите число параллельных браузеров, прежде чем повторять. - 502, 503, 504: прокси не смог достучаться до исходящего выхода или выбрать его. Немедленный повтор редко помогает; запишите время и спросите у провайдера, что зафиксировал в логах его шлюз.
connect=000 с кодом выхода curl, отличным от 0, означает, что сбой произошёл до какого-либо ответа на CONNECT, что соответствует строкам ERR_PROXY_CONNECTION_FAILED и сброса соединения выше. Руководство по таймаутам показывает, как измерить каждый этап, когда сбой медленный, а не мгновенный.
Playwright: 407 не выбрасывает исключение
Без учётных данных прокси или с неверными page.goto в записанном прогоне не отклонялся. Он завершался ответом со статусом 407 и statusText Proxy Authentication Required, с читаемым на этом ответе заголовком Proxy-Authenticate от прокси и пустым телом; для той же навигации срабатывало событие requestfailed с ERR_TUNNEL_CONNECTION_FAILED. Поэтому скрипт, который лишь дожидается goto и идёт дальше, продолжает работать с пустой страницей. Проверяйте response.ok() и считайте 407 проблемой прокси, а не целевого сервера:
import { chromium } from 'playwright';
function required(name) {
const value = process.env[name];
if (!value) throw new Error('Missing environment variable: ' + name);
return value;
}
const browser = await chromium.launch({
proxy: {
server: required('PROXY_URL'), // http://host:port, no credentials in the URL
username: required('PROXY_USERNAME'),
password: required('PROXY_PASSWORD'),
},
});
try {
const page = await browser.newPage();
page.on('requestfailed', (request) => {
console.error('request failed:', request.failure()?.errorText, request.url());
});
const response = await page.goto('https://example.com/', { waitUntil: 'domcontentloaded' });
if (!response) throw new Error('No navigation response');
if (response.status() === 407) throw new Error('Proxy rejected the credentials (407)');
if (!response.ok()) throw new Error('Destination answered ' + response.status());
} finally {
await browser.close();
}Задавайте учётные данные прокси в proxy.username и proxy.password. В записанном прогоне учётные данные, переданные через опцию контекста httpCredentials, тоже удовлетворяли вызов прокси, потому что Playwright отвечает на запрос аутентификации Chromium из любого источника. Не полагайтесь на это: httpCredentials предназначены для целевого сервера, и их повторное использование для прокси отправляет логин от сайта оператору прокси.
Ещё две детали Playwright из прогона. Playwright сам добавляет <-loopback> в список обхода, поэтому назначение на localhost или 127.0.0.1 идёт через прокси; сырой Chromium делает наоборот (см. раздел о Puppeteer). А сервер socks5:// с именем пользователя и паролем отклоняется ещё до запуска браузера с сообщением Browser does not support socks5 proxy authentication. В Chromium нет поддержки аутентификации SOCKS5, поэтому решение — HTTP-шлюз с Basic-аутентификацией или списком разрешённых IP-адресов, а не другая настройка SOCKS. Руководство HTTP vs SOCKS5 описывает, что меняется между ними.
Puppeteer: аутентифицируйтесь до первой навигации
Puppeteer передаёт прокси в Chromium флагом и отвечает на вызовы 407 только после вызова page.authenticate. Без него записанный прогон давал net::ERR_INVALID_AUTH_CREDENTIALS — код, который многие никак не связывают с прокси. При неверных учётных данных goto завершался ответом со статусом 407 и встроенной страницей ошибки Chromium в качестве тела, плюс событием requestfailed с ERR_HTTP_RESPONSE_CODE_FAILURE.
import puppeteer from 'puppeteer';
function required(name) {
const value = process.env[name];
if (!value) throw new Error('Missing environment variable: ' + name);
return value;
}
const browser = await puppeteer.launch({
args: [
'--proxy-server=' + required('PROXY_URL'), // http://host:port
'--proxy-bypass-list=<-loopback>', // only if local destinations must use the proxy
],
});
try {
const page = await browser.newPage();
await page.authenticate({
username: required('PROXY_USERNAME'),
password: required('PROXY_PASSWORD'),
});
page.on('requestfailed', (request) => {
console.error('request failed:', request.failure()?.errorText, request.url());
});
const response = await page.goto('https://example.com/', { waitUntil: 'domcontentloaded' });
if (!response) throw new Error('No navigation response');
if (response.status() === 407) throw new Error('Proxy rejected the credentials (407)');
if (!response.ok()) throw new Error('Destination answered ' + response.status());
} finally {
await browser.close();
}Здесь важны два значения Chromium по умолчанию. Во-первых, Chromium обходит прокси для localhost, 127.0.0.1 и [::1], если список обхода не содержит <-loopback>. В прогоне скрипт Puppeteer, направленный на прокси, который отклонял каждый туннель, всё равно загрузил локальную HTTPS-страницу со статусом 200, потому что к прокси никто не обращался. Тест, который «проходит» на локальном сервере, ничего не доказывает о прокси. Во-вторых, сырой Chromium, запущенный с --proxy-server, направлял через прокси и собственный фоновый трафик: в записанном логе прокси видны GET к clients2.google.com и CONNECT к update.googleapis.com:443 до навигации на страницу. Ожидайте эти имена хостов в логах провайдера и в учитываемом трафике и не принимайте их сбои за сбой вашей навигации. Флаги запуска Playwright в том же прогоне такого трафика не создавали.
Ошибки, которые не являются ошибками туннеля
ERR_CONNECTION_RESETилиERR_CONNECTION_CLOSEDсразу после подключения: прокси принял TCP-соединение и оборвал его — либо до ответа наCONNECT, либо сразу после 200. Провайдеры делают так, когда выход недоступен или соединение ограничивается по частоте на уровне сокета. curl для того же поведения сообщаетProxy CONNECT abortedилиSend failure: Broken pipe.ERR_INVALID_HTTP_RESPONSE: на этом порту что-то ответило, но не по HTTP. Обычно номер порта принадлежит SOCKS-слушателю, слушателю только для TLS или другому сервису.ERR_SOCKS_CONNECTION_FAILED: вы настроилиsocks5://, а сервер говорил на HTTP. Смените схему на документированный протокол шлюза, прежде чем менять что-либо ещё.- Таймаут навигации без сетевой ошибки: прокси принял соединение и так и не ответил. Увеличение таймаута не поможет; проверьте порт и протокол с помощью curl, который в той же ситуации завершается по таймауту с
(28). ERR_PROXY_CONNECTION_FAILEDна шлюзе, который работает в curl: сравните схему. Chromium пытается установить TLS для проксиhttps://, а большинство шлюзов — это обычные конечные точкиhttp://, которые несут HTTPS-назначения внутри туннеля.
Порядок диагностики
- Воспроизведите с помощью curl с теми же шлюзом, целевым сервером и учётными данными и прочитайте
http_connect. Это меньше чем за минуту превращает односложную ошибку браузера в фактический статус прокси. - Сверьте схему прокси (
http://,https://илиsocks5://) с актуальными инструкциями провайдера. Две из трёх строкERR_PROXY_CONNECTION_FAILEDвыше — ошибки в схеме. - Размещайте учётные данные там, где их ожидает инструмент:
proxy.usernameиproxy.passwordв Playwright,page.authenticateдо первой навигации в Puppeteer. В обоих явно проверяйтеresponse.status() === 407. - Тестируйте HTTPS-назначение, а не HTTP. HTTP-назначения пропускают
CONNECTи прячут проблемы туннеля за обычными ответами прокси. - Проверьте правила обхода. Локальные и внутренние назначения в сыром Chromium обходят прокси, а корпоративные настройки или настройки прокси из окружения могут переопределить то, что вы передали.
- Сообщайте версии клиентов, время в UTC, хост и порт шлюза, имя целевого хоста и значение curl
http_connect. Не включайте пароли, URL с учётными данными и сырые трассировки; в руководстве по ошибке 407 есть шаблон очищенного отчёта.
Методика и файлы для загрузки
ipvolt выполнил воспроизведение 17 сентября 2026 года на Linux-хосте с Node.js 24.20.0, Playwright 1.63.0 (Chromium 153.0.8010.12, headless), puppeteer-core 25.11.0, управляющим той же сборкой Chromium, и curl 8.18.0. Каждый «прокси» был небольшим сервером на Node.js на 127.0.0.1 ровно с одним поведением; назначением служил локальный HTTPS-сервер с одноразовым самоподписанным сертификатом, поэтому в браузерных случаях ошибки сертификата целевого сервера игнорировались, что не влияет на то, как Chromium общается с прокси. Firefox и WebKit не проверялись, коммерческий шлюз не задействовался: таблица описывает реакцию Chromium на поведение прокси, а не политику какого-либо провайдера.
Архив лаборатории содержит скрипт лаборатории, манифест пакета, README и записанные результаты, включая лог на стороне прокси, который показывает, дошёл ли каждый клиент до прокси и пришли ли учётные данные. Он работает только на loopback и не требует аккаунта прокси.
Если хотите узнать, когда откроется доступ к ipvolt, присоединяйтесь к списку раннего доступа. Одно письмо, когда доступ откроется. Больше ничего. Эти примеры — клиентская диагностика, а не документация доступной конечной точки ipvolt.
Источники и дополнительное чтение
Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.