Код 407 указывает на уровень аутентификации прокси. Начните с него, прежде чем менять запрос к целевому серверу или добавлять повторы. В этом руководстве — порядок проверок, точный симптом в curl, Python-клиентах requests, httpx, aiohttp и urllib3, в Playwright и в Puppeteer, и по одному рабочему примеру для каждого клиента. Каждая строка ошибки и каждый пример ниже записаны на маленьком loopback-прокси, который требует Basic-аутентификацию; ссылки на лабораторный стенд и запись прогона приведены в конце.
Определите, кто запрашивает учётные данные
HTTP 407 означает, что прокси требует аутентификации. Ответ содержит вызов Proxy-Authenticate, а клиент передаёт учётные данные прокси через Proxy-Authorization, как определено в RFC 9110. Аутентификация на целевом сервере — отдельный механизм: собственный вход на сайт использует 401 и Authorization. Подставлять пароль прокси в заголовок Authorization целевого сайта или в параметр аутентификации на сайте в вашем клиенте — неверное решение; в каждом разделе про клиент ниже этот параметр назван явно, чтобы вы могли его исключить.
Где появится 407, зависит от назначения. Для назначения http:// прокси сам пересылает запрос, поэтому 407 приходит как обычный ответ: код состояния 407, заголовок Proxy-Authenticate, иногда короткое тело. Для назначения https:// клиент сначала отправляет CONNECT host:443, чтобы открыть туннель. 407 на этом шаге случается до какого-либо обмена с назначением, поэтому большинство клиентов бросают исключение прокси или туннеля, а не отдают вам объект ответа. Например, curl 8.15, 8.16 и 8.17 печатают curl: (56) CONNECT tunnel failed, response 407; curl 8.18.0 и 8.22.0 в наших прогонах печатали то же сообщение с кодом выхода 7. Один и тот же прокси, те же учётные данные, разные симптомы:
| Клиент | Назначение https:// (CONNECT) | Назначение http:// |
|---|---|---|
| requests 2.34 | requests.exceptions.ProxyError с Tunnel connection failed: 407 Proxy Authentication Required | Response со status_code == 407 |
| httpx 0.28 | httpx.ProxyError: 407 Proxy Authentication Required | Response со status_code == 407 |
| aiohttp 3.14 | aiohttp.ClientHttpProxyError: 407, message='Proxy Authentication Required' | ответ со status == 407 |
| urllib3 2.8 | urllib3.exceptions.ProxyError с Tunnel connection failed: 407 | ответ со status == 407 |
| Playwright 1.63 | с waitUntil: "load" по умолчанию page.goto отклоняется с Timeout 30000ms exceeded, а requestfailed сообщает net::ERR_TUNNEL_CONNECTION_FAILED; с waitUntil: "commit" он разрешается со status() === 407 | page.goto разрешается со статусом 407 и телом ошибки от прокси |
| Puppeteer 25 | page.goto отклоняется с net::ERR_INVALID_AUTH_CREDENTIALS (без учётных данных) или разрешается со статусом 407 плюс net::ERR_HTTP_RESPONSE_CODE_FAILURE (неверные учётные данные) | отклоняется с net::ERR_INVALID_AUTH_CREDENTIALS |
| curl 8.15–8.17 | exit 56, curl: (56) CONNECT tunnel failed, response 407, %{http_connect} равен 407 | http=407 |
| curl 8.18.0 | exit 7, curl: (7) CONNECT tunnel failed, response 407, %{http_connect} равен 407 | http=407 |
Если вместо 407 вы видите 429, 502 или 504, статья про коды ошибок прокси показывает, какой слой выдал этот код. Когда Chromium сообщает о сбое CONNECT без статуса, руководство по ошибкам туннеля разбирает остальное семейство net::ERR_*. Scrapy в таблицу не вошёл: Scrapy 2.19 для URL https:// выбрасывает TunnelError: Could not open CONNECT tunnel with proxy, а ответ 407 для URL http:// отбрасывает, оставляя в журнале одну строку уровня INFO; оба случая разобраны в руководстве Прокси в Scrapy 2.19. curl_cffi в таблицу тоже не вошёл: версия 0.16.3 для URL https:// выбрасывает ConnectionError с текстом curl: (7) CONNECT tunnel failed, response 407, а не ProxyError, а для URL http:// возвращает 407 как обычный ответ; как перехватить оба случая, показано в руководстве Прокси в curl_cffi.
Проверяйте по одной переменной за раз
Используйте этот порядок, чтобы получить полезное воспроизведение, а не череду несвязанных изменений. Чаще всего сюрпризом оказываются унаследованные настройки; руководство по переменным окружения прокси объясняет, как увидеть, какой шлюз клиент выбрал на самом деле.
- Шлюз: скопируйте точную схему, имя хоста и порт из актуальных инструкций по настройке в аккаунте.
- Метод: уточните, ожидает ли эта конечная точка имя пользователя и пароль, разрешённый исходный IP-адрес или другой документированный механизм аутентификации. На пароль, отправленный на порт с аутентификацией по исходному IP, с новой машины приходит 407.
- Область учётных данных: убедитесь, что имя пользователя относится к этому продукту или субпользователю, а не просто к логину в панели управления.
- Кодирование: применяйте percent-encoding к учётным данным при встраивании их в URL; используйте отдельные поля для учётных данных, если клиент их поддерживает.
- Клиент: проверьте унаследованные настройки прокси и убедитесь, что реально выбран нужный шлюз. Устаревшая
HTTPS_PROXY,.curlrc, окружение контейнера или секрет CI могут указывать на другой шлюз или старый пароль. Логируйте хост и порт, которые клиент разрешил, но никогда — пароль.
Начните с одного запроса через curl
Прежде чем трогать код приложения, убедитесь, что шлюз, имя пользователя и пароль вообще работают. Учётные данные прокси отделены от учётных данных сайта: -U или --proxy-user отправляет их прокси; -u или --user — целевому сайту. С неправильным флагом прокси по-прежнему не видит Proxy-Authorization и отвечает 407, хотя пароль верный.
curl --disable -sS -o /dev/null --noproxy '' \
-x "$PROXY_URL" --proxy-user "$PROXY_USERNAME:$PROXY_PASSWORD" \
-w 'http=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' \
https://example.com/connect=200 http=200 означает, что учётные данные приняты и проблема в вашем приложении. connect=407 вместе с CONNECT tunnel failed, response 407 означает, что не принимается сама учётная запись; проверьте шлюз, метод и область учётных данных из списка выше, затем отправьте отчёт, описанный в конце. В руководстве по curl с прокси есть полный справочник флагов.
Сравните curl с приложением, в котором возникает ошибка
Если запрос через curl работает, сосредоточьтесь на том, как приложение отображает эти настройки. В Playwright, например, учётные данные прокси задаются в proxy.username и proxy.password, а не в httpCredentials. Разделы про клиенты ниже показывают точное исключение или статус, которые выдаёт каждый клиент, и рабочий пример для каждого.
Если оба клиента завершаются ошибкой, уберите необязательные модификаторы страны или сессии, специфичные для провайдера, и проверьте самый простой документированный формат учётных данных, затем добавляйте по одному модификатору за раз. Подтвердите у провайдера доступ к аккаунту и инструкции по аутентификации для конечной точки. Не предполагайте, что все провайдеры используют 407 для одного и того же условия оплаты или доступа к продукту.
Python requests: ProxyError, Tunnel connection failed: 407
Requests берёт учётные данные прокси только из URL прокси. Отдельного аргумента для аутентификации на прокси, который переживал бы шаг CONNECT, нет: requests.auth.HTTPProxyAuth добавляет заголовок Proxy-Authorization к запросу, что работает для назначения http:// и ничего не даёт для https://, потому что туннель открывает urllib3 ещё до того, как этот заголовок будет отправлен. То же относится к записи Proxy-Authorization в headers=. В записанном прогоне оба варианта вернули 200 для http://example.com/ и ProxyError для https://example.com/.
Ещё три вещи ломают форму с URL:
- Специальные символы. Пароль вроде
p@ss/w0rd#1, вставленный как есть вhttp://user:p@ss/w0rd#1@host:port, заставляет парсер URL считать хостомss. Requests тогда падает сNameResolutionErrorдля хоста'ss', что совсем не похоже на проблему аутентификации. Кодируйте имя пользователя и пароль по отдельности черезurllib.parse.quote(value, safe=""). - Оба ключа. Словарь
proxiesиндексируется по схеме назначения. Словарь только с записью"http"отправляетhttps://-запросы напрямую, вообще без прокси. Задайте для"http"и"https"один и тот же URL с учётными данными. - Переменные окружения сильнее Session. Если в окружении задана
HTTPS_PROXY,Session.proxiesей проигрывает, и переменная без учётных данных превращает правильную конфигурацию Session в 407. Аргументproxies=у самого запроса всё же побеждает. Самое чистое решение —session.trust_env = False, которое заодно отключает наследованиеNO_PROXY; руководство по переменным окружения объясняет, что ещё контролирует эта настройка.
Пример ниже учитывает все три пункта. Это файл fix_requests.py из архива лаборатории; он запускался с обычным паролем, с паролем со специальными символами и с неверным паролем: первые два напечатали 200, третий — сообщение ProxyError.
import os
from urllib.parse import quote
import requests
# PROXY_URL is scheme://host:port with no credentials inside, for example http://gate.example.net:8080
proxy_url = os.environ["PROXY_URL"]
username = quote(os.environ["PROXY_USERNAME"], safe="")
password = quote(os.environ["PROXY_PASSWORD"], safe="")
target = os.environ.get("TARGET_URL", "https://example.com/")
scheme, host_port = proxy_url.split("://", 1)
authenticated_proxy = f"{scheme}://{username}:{password}@{host_port}"
proxies = {"http": authenticated_proxy, "https": authenticated_proxy}
session = requests.Session()
session.trust_env = False # HTTP_PROXY, HTTPS_PROXY and NO_PROXY can no longer override these proxies
try:
response = session.get(target, proxies=proxies, timeout=(5, 15))
print(response.status_code)
except requests.exceptions.ProxyError as error:
print("proxy rejected the request:", error)Руководство по прокси для Python Requests поверх этого разбирает сессии, SOCKS и ротацию; пример здесь — минимум, при котором 407 исчезает.
Python httpx: httpx.ProxyError: 407 Proxy Authentication Required
У httpx для этого есть отдельный объект: httpx.Proxy(url, auth=(username, password)). Учётные данные попадают в запрос CONNECT и никогда не касаются URL, поэтому пароль со специальными символами не нуждается в кодировании. Аргумент auth= у httpx.get или у Client — это аутентификация на сайте; в записанном прогоне он оставил вызов прокси без ответа и поднял httpx.ProxyError: 407 Proxy Authentication Required. Учётные данные внутри URL прокси тоже работают, если они percent-encoded; вставленный как есть, тот же пароль p@ss/w0rd#1 дал httpx.ConnectError: [Errno -3] Temporary failure in name resolution — снова потому, что парсер прочитал пароль как имя хоста.
import os
import httpx
proxy = httpx.Proxy(
url=os.environ["PROXY_URL"], # scheme://host:port, no credentials inside
auth=(os.environ["PROXY_USERNAME"], os.environ["PROXY_PASSWORD"]),
)
target = os.environ.get("TARGET_URL", "https://example.com/")
with httpx.Client(proxy=proxy, trust_env=False, timeout=15.0) as client:
try:
response = client.get(target)
print(response.status_code)
except httpx.ProxyError as error:
print("proxy rejected the request:", error)httpx.AsyncClient принимает тот же аргумент proxy=, как и httpx.HTTPTransport, если через прокси вы пускаете только часть URL-шаблонов с помощью mounts. Оба варианта проверялись в лаборатории и вернули 200. Для полностью асинхронного воркера руководство по асинхронному httpx с прокси строится на том же объекте Proxy.
Python aiohttp: ClientHttpProxyError 407
aiohttp явно разделяет две пары учётных данных: proxy_auth=aiohttp.BasicAuth(username, password) — для прокси, auth= — для сайта. Передача логина прокси через auth= в записанном прогоне подняла aiohttp.client_exceptions.ClientHttpProxyError: 407, message='Proxy Authentication Required'. Учётные данные, встроенные в URL прокси, тоже принимаются. В aiohttp 3.14 proxy_auth и BasicAuth объявлены устаревшими: вызов ниже по-прежнему работает, но печатает два DeprecationWarning. В руководстве Прокси в aiohttp 3.14 показана замена и случай, когда она не авторизует запрос.
Одна ловушка, характерная именно для aiohttp: ClientSession игнорирует HTTP_PROXY и HTTPS_PROXY, если не создать его с trust_env=True. При значении по умолчанию сессия без аргумента proxy= подключается напрямую, поэтому скрипт, который «на другой машине работал, потому что там была задана переменная», молча перестаёт ходить через прокси вместо того, чтобы упасть с 407.
import asyncio
import os
import aiohttp
async def main() -> None:
proxy_url = os.environ["PROXY_URL"] # scheme://host:port, no credentials inside
proxy_auth = aiohttp.BasicAuth(os.environ["PROXY_USERNAME"], os.environ["PROXY_PASSWORD"])
target = os.environ.get("TARGET_URL", "https://example.com/")
timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.get(target, proxy=proxy_url, proxy_auth=proxy_auth) as response:
print(response.status)
except aiohttp.ClientHttpProxyError as error:
print("proxy rejected the request:", error.status, error.message)
asyncio.run(main())urllib3 и urllib.request
Если вы используете urllib3 напрямую, ProxyManager не читает учётные данные из URL прокси для туннеля: ProxyManager("http://user:pass@host:port") всё равно дал ProxyError с Tunnel connection failed: 407 для назначения https://. Вместо этого передавайте их как proxy_headers=urllib3.util.make_headers(proxy_basic_auth="user:pass"). Аргумент headers= у request() для CONNECT тоже не используется. Стандартный urllib.request.ProxyHandler — противоположный случай: он разбирает http://user:pass@host:port и в том же прогоне ответил на вызов корректно.
Playwright: Timeout exceeded, статус 407 и net::ERR_TUNNEL_CONNECTION_FAILED
Playwright настраивает прокси через опцию proxy — либо в browserType.launch, либо в browser.newContext — с тремя полями: server, username и password. Когда учётных данных нет или они неверны, симптом зависит от того, чего ждёт page.goto. С waitUntil: "load" по умолчанию и с "domcontentloaded" документ так и не загружается, поэтому вызов отклоняется с page.goto: Timeout 30000ms exceeded по истечении полного таймаута навигации, и единственная подсказка — событие requestfailed с net::ERR_TUNNEL_CONNECTION_FAILED. Именно поэтому 407 от прокси в Playwright так часто описывают как «прокси тормозит». С waitUntil: "commit" та же навигация разрешается за миллисекунды ответом, у которого status() равен 407, statusText() — Proxy Authentication Required, а среди заголовков есть Proxy-Authenticate от прокси. Пример ниже сначала ждёт commit, проверяет статус и только потом ждёт domcontentloaded; в записанном прогоне случай с неверным паролем упал меньше чем за секунду с сообщением про 407, а не через тридцать секунд.
Два варианта размещения выглядят правильно, но таковыми не являются:
- Учётные данные внутри
server.{ server: "http://user:pass@host:port" }принимается без возражений, и прокси затем получает CONNECT вообще безProxy-Authorization. Chromium игнорирует часть userinfo в URL прокси. httpCredentialsна контексте. В записанном прогоне Chromium 153 действительно ответил на вызов прокси с помощьюhttpCredentials, и навигация прошла. Это всё равно неправильное место:httpCredentials— это вход на сайт, и Playwright предложит те же имя пользователя и пароль любому origin, который ответит 401. Держите пароль прокси вproxy.usernameиproxy.password.
import { chromium } from 'playwright';
const proxy = {
server: process.env.PROXY_URL, // scheme://host:port, no credentials inside
username: process.env.PROXY_USERNAME, // proxy credentials belong here, not in httpCredentials
password: process.env.PROXY_PASSWORD,
};
const target = process.env.TARGET_URL ?? 'https://example.com/';
const browser = await chromium.launch();
try {
const context = await browser.newContext({ proxy });
const page = await context.newPage();
page.on('requestfailed', (request) => console.error('failed:', request.url(), request.failure()?.errorText));
// Wait for 'commit' first: a proxy 407 never reaches 'domcontentloaded', it would surface as a TimeoutError instead.
const response = await page.goto(target, { timeout: 30_000, waitUntil: 'commit' });
if (!response || response.status() === 407) {
throw new Error(`proxy rejected the credentials: ${response?.status()} ${response?.statusText()}`);
}
await page.waitForLoadState('domcontentloaded');
console.log(response.status());
} finally {
await browser.close();
}Для прокси на уровне контекста сначала нужен chromium.launch({ proxy: { server: 'per-context' } }); лабораторный прогон подтверждает, что настроенный так контекст аутентифицируется самостоятельно. API request.newContext, используемый для API-тестов, принимает точно такой же объект proxy и вернул 200 с учётными данными и 407 без них. Специальные символы в пароле здесь кодировать не нужно, потому что Playwright никогда не собирает из них URL. Одно ограничение — не со стороны Playwright: Chromium не поддерживает имя пользователя и пароль для SOCKS5-прокси, как сказано в его документации по прокси, так что шлюз socks5://, требующий входа, из Chromium использовать нельзя вовсе, и руководство HTTP против SOCKS5 поможет выбрать вместо него HTTP-шлюз. Руководство по прокси в Playwright показывает, как держать эту конфигурацию изолированной для каждого сценария.
Puppeteer: net::ERR_INVALID_AUTH_CREDENTIALS
Puppeteer передаёт прокси в Chromium флагом запуска --proxy-server=host:port, а флага для пароля нет. Без вызова page.authenticate первая навигация отклоняется с net::ERR_INVALID_AUTH_CREDENTIALS; так Chromium сообщает, что пришёл вызов от прокси, а отвечать на него никто не зарегистрирован. С неверным паролем page.goto разрешается со статусом 407 и собственной страницей ошибки Chromium в документе, а requestfailed сообщает net::ERR_HTTP_RESPONSE_CODE_FAILURE.
Что не работает, по данным записанного прогона:
--proxy-server=http://user:pass@host:portсразу падает сnet::ERR_NO_SUPPORTED_PROXIES.page.setExtraHTTPHeaders({ 'Proxy-Authorization': ... })падает сnet::ERR_INVALID_ARGUMENT; Chromium не позволяет коду страницы задавать этот заголовок.
Что работает: вызовите page.authenticate({ username, password }) на странице до её первой навигации. После этого учётные данные кэшируются для контекста браузера, так что вторая страница, открытая через browser.newPage(), загрузилась без собственного вызова, а страница в новом контексте из browser.createBrowserContext() падала с ERR_INVALID_AUTH_CREDENTIALS, пока не получила свой. Вызов page.authenticate после неудачной навигации с повтором page.goto тоже сработал — это полезно, когда учётные данные становятся известны позже.
import puppeteer from 'puppeteer';
const target = process.env.TARGET_URL ?? 'https://example.com/';
const browser = await puppeteer.launch({
args: [`--proxy-server=${process.env.PROXY_URL}`], // scheme://host:port, credentials are not accepted here
});
try {
const page = await browser.newPage();
// Register the proxy credentials before the first navigation; every new browser context needs its own call.
await page.authenticate({ username: process.env.PROXY_USERNAME, password: process.env.PROXY_PASSWORD });
const response = await page.goto(target, { timeout: 30_000, waitUntil: 'domcontentloaded' });
if (!response || response.status() === 407) {
throw new Error(`proxy rejected the credentials: ${response?.status()}`);
}
console.log(response.status());
} finally {
await browser.close();
}Node.js-коду, которому нужен только HTTP, а не браузер, не стоит ради этого запускать Chromium; руководство по fetch с прокси в Node.js ставит те же учётные данные на ProxyAgent из Undici.
Отправьте воспроизводимый отчёт без секретов
Сохраните версию клиента, метку времени в UTC, хост и порт шлюза, имя целевого хоста, метод аутентификации и статус или класс исключения. Укажите, завершается ли ошибкой и минимальное воспроизведение через curl. Эти детали позволяют поддержке найти попытку, не получая вашего пароля.
Удалите из логов токены, URL с учётными данными, cookie и заголовки авторизации. Отключите автоматические повторы на время диагностики устойчивой ошибки 407: цикл повторов превращает один 407 в шквал неудачных входов, а статья один потерянный ответ, две созданные задачи объясняет, почему слепые повторы вредят и по другим причинам. После исправления выполните один запрос, затем наименьший сценарий приложения и только потом возвращайте обычный уровень параллелизма.
Для помощи внутри совместимого с MCP агента для программирования Proxy Toolkit MCP предлагает диагностику ошибок и поддерживаемые шаблоны конфигурации. Опишите клиент и очищенную ошибку, не передавая учётные данные прокси и полные логи; проверьте любое предложенное изменение ограниченным воспроизведением выше.
- Отличайте аутентификацию на прокси от аутентификации на сайте.
- Воспроизведите с теми же настройками во втором клиенте.
- Делитесь очищенной диагностикой, а не учётными данными или сырыми трассировками.
Воспроизведите записанный прогон
Симптомы клиентов и примеры выше записаны с помощью лаборатории из папки загрузок: поддельный прокси на 127.0.0.1, который требует Basic-аутентификацию и пересылает запросы на https://example.com/, скрипт symptoms.py, воспроизводящий каждую строку ошибки Python, случаи для Playwright, Puppeteer и curl, а также пять примеров fix_*, запущенных с обычным паролем, паролем со специальными символами и неверным паролем. Записанный results.json называет использованные версии: Python 3.14, requests 2.34, httpx 0.28, aiohttp 3.14, urllib3 2.8, Node.js 24, Playwright 1.63 с Chromium 153, Puppeteer 25 и curl 8.18. Сообщения curl 8.15–8.17 и 8.22.0 взяты из loopback-лаборатории, опубликованной вместе со статьёй CONNECT tunnel failed, response 403. Более новые релизы могут изменить строку ошибки; механизм — учётные данные прокси в опции прокси и нигде больше — не меняется.
Источники и дополнительное чтение
Технические материалы, использованные при подготовке руководства. Сверяйтесь с документацией вашей версии и с поддерживаемой конфигурацией вашего провайдера.
- RFC 9110: 407 Proxy Authentication Required
- Everything curl: proxy authentication
- Playwright: proxy credentials
- Requests documentation: proxies
- HTTPX documentation: proxies
- aiohttp documentation: proxy support
- urllib3 documentation: proxies
- Puppeteer API: page.authenticate
- Chromium proxy support: SOCKS authentication