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

Как исправить ошибку прокси 407, не гадая

Разбираем ошибку Proxy Authentication Required по шагам: шлюз, метод аутентификации, кодирование учётных данных, область действия аккаунта и настройки клиента.

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

Код 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.34requests.exceptions.ProxyError с Tunnel connection failed: 407 Proxy Authentication RequiredResponse со status_code == 407
httpx 0.28httpx.ProxyError: 407 Proxy Authentication RequiredResponse со status_code == 407
aiohttp 3.14aiohttp.ClientHttpProxyError: 407, message='Proxy Authentication Required'ответ со status == 407
urllib3 2.8urllib3.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() === 407page.goto разрешается со статусом 407 и телом ошибки от прокси
Puppeteer 25page.goto отклоняется с net::ERR_INVALID_AUTH_CREDENTIALS (без учётных данных) или разрешается со статусом 407 плюс net::ERR_HTTP_RESPONSE_CODE_FAILURE (неверные учётные данные)отклоняется с net::ERR_INVALID_AUTH_CREDENTIALS
curl 8.15–8.17exit 56, curl: (56) CONNECT tunnel failed, response 407, %{http_connect} равен 407http=407
curl 8.18.0exit 7, curl: (7) CONNECT tunnel failed, response 407, %{http_connect} равен 407http=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, хотя пароль верный.

sh
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.

python
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 — снова потому, что парсер прочитал пароль как имя хоста.

python
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.

python
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.
javascript
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 тоже сработал — это полезно, когда учётные данные становятся известны позже.

javascript
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. Более новые релизы могут изменить строку ошибки; механизм — учётные данные прокси в опции прокси и нигде больше — не меняется.

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

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