VPN работает, внутренняя панель открывается в браузере, а скрипт Python всё равно выдаёт ошибку прокси. В этой картине с туннелем всё в порядке: интерпретатор читает адрес прокси из собственного окружения и отправляет туда запрос. Это руководство читает окружение, которое процесс действительно видит, применяет самое узкое исключение и только потом рассматривает явную политику сеанса. Каждый шаг заканчивается командой, вывод которой решает, продолжать ли.

Прочитайте окружение, которое процесс действительно видит

Настройки прокси на рабочем столе, экспорты в оболочке и строки Environment= модуля systemd — это три разных источника, и Requests читает только последний, который наследует. Запустите диагностику ниже в той же оболочке, контейнере или модуле, который запускает сбойный скрипт, а не в своём интерактивном терминале. Переменная, экспортированная в вашей оболочке входа, невидима для процесса в контейнере, и этот разрыв объясняет множество сообщений о прокси, который появляется ниоткуда. Обе команды печатают одни и те же данные с разной степенью детализации.

env | grep -i -E 'proxy|requests_ca_bundle|curl_ca_bundle'
python3 -c "import os;[print(k,'=',v) for k,v in sorted(os.environ.items()) if 'proxy' in k.lower()]"

Читаются оба написания каждой переменной, а дубликаты применяются в том порядке, в котором процесс их получил, поэтому устаревший HTTP_PROXY может перекрыть только что экспортированное значение. Переменные пакета CA здесь тоже важны, потому что они меняют, во что разрешается verify, не появляясь нигде в коде. Если вывод пуст, окружение не ваша проблема, и этот раздел можно не читать. Если в нём перечислен прокси, который вы не узнаёте, найдите файл или модуль, который его задаёт, прежде чем что-либо менять.

Спросите у Requests, что он сделает с конкретным URL. Этот вызов показывает объединённые значения прокси, verify и cert для такого запроса, не отправляя пакет.

python3 -c "import requests;s=requests.Session();print(s.merge_environment_settings('https://api.corp.example/status',{},None,None,None))"

Непустая запись proxies для частного хоста и есть ваш ответ, а напечатанное значение verify говорит, какому пакету CA запрос доверял бы. Если оба выглядят верно, сбой находится дальше в стеке, и вам следует перейти к разделу проверки. Не гадайте по коду: объединение выполняется для каждого запроса, поэтому один сеанс может вести себя по-разному для двух хостов.

Исключите частный хост, прежде чем что-либо отключать

Когда прокси действительно нужен для других назначений, узкое исправление — исключение для этого хоста. Добавляйте к существующему списку, а не заменяйте его, потому что стирание NO_PROXY ломает любое другое внутреннее назначение, которое на него полагалось. Команды ниже добавляют запись и затем спрашивают у Requests, действует ли обход.

export NO_PROXY="${NO_PROXY:+$NO_PROXY,}api.corp.example,.corp.example"
python3 -c "import requests;print(requests.utils.should_bypass_proxies('https://api.corp.example/status',None))"

Сопоставление идёт по суффиксу хоста, поэтому .corp.example покрывает весь домен, а api.corp.example — одно имя. Голая звёздочка отключает прокси для любого хоста и почти никогда не то, чего хочет производственная задача. Учитывается ли запись CIDR, зависит от версий Requests и urllib3 в образе, поэтому проверяйте точную запись, а не предполагайте, что она работает. Проверяйте результат should_bypass_proxies для реального URL, а не для имени хоста, набранного по памяти.

Держите два слоя раздельно в голове. Обход прокси HTTP не обходит VPN, потому что решение о прокси принимается в библиотеке, а решение о туннеле — в таблице маршрутизации ядра.

Отключайте trust_env только как осознанную политику сеанса

Если вся задача должна игнорировать окружающую конфигурацию, постройте сеанс явно и зафиксируйте решение в коде. Это политика, а не обходной путь, и ей место в diff, который видит рецензент. Пример ниже сохраняет тайм-аут, явный путь к CA и проверку статуса.

import requests

with requests.Session() as session:
    session.trust_env = False
    response = session.get(
        "https://api.corp.example/status",
        timeout=(5, 20),
        verify="/etc/ssl/certs/corp-ca.pem",
    )
    response.raise_for_status()

Отключение trust_env меняет три поведения сразу, и только одно из них касается прокси. Requests перестаёт читать учётные данные .netrc, а переменные REQUESTS_CA_BUNDLE и CURL_CA_BUNDLE перестают влиять на verify, который затем возвращается к встроенному хранилищу сертификатов. Если ваша организация завершает TLS своим CA, этот откат вызывает ошибку проверки, которая выглядит не связанной с только что сделанным изменением. Именно явная передача verify удерживает два эффекта раздельно.

Чего делать не следует — это задавать verify=False, чтобы заглушить возникшую ошибку. Так неправильная конфигурация прокси обменивается на непроверенное соединение, и скрывается проблема сертификата, которую исключение должно было выявить.

Проверьте сбойный слой и сохраните возможность отката

Три неисправности выглядят в журнале одинаково и не имеют ничего общего: прокси, не достигающий назначения, выдаёт ProxyError или возвращает 502, проблема доверия выдаёт SSLError, а отказ приложения возвращает 401 или 403. Воспроизведите запрос вне Requests, чтобы увидеть, какой из них у вас. Команды ниже убирают прокси, проверяют цепочку сертификатов и затем повторяют вызов через библиотеку.

curl -sv --noproxy '*' https://api.corp.example/status -o /dev/null
openssl s_client -connect api.corp.example:443 -servername api.corp.example </dev/null
python3 -c "import requests;r=requests.get('https://api.corp.example/status',timeout=10);print(r.status_code)"

Если вызов curl успешен, а вызов Python завершается ошибкой, разница всё ещё в окружении. Если оба сбоят на этапе TLS, прокси никогда не был проблемой, и проблема в цепочке сертификатов. Если оба возвращают 401, запрос дошёл до приложения, и оставшаяся работа — аутентификация. Запись того, какой из трёх случаев вы наблюдали, экономит следующему человеку час.

Откат — это одна строка в любую сторону: уберите исключение или верните trust_env назад. Сохраните предыдущее значение NO_PROXY, прежде чем что-то добавлять, и предпочитайте скрипт-обёртку или запись Environment= на уровне модуля экспорту, который живёт только в истории вашего терминала. Оба изменения видны в окружении процесса, поэтому перезапуск обычно возвращает вас к старому поведению.

Завершите, подтвердив четыре вещи: объединённые настройки для частного URL не показывают прокси, сертификат проверяется по нужному CA, запрос возвращает ожидаемый статус, а изменение зафиксировано там, где его можно воспроизвести. Зелёный скрипт с необъяснённым окружением — это исправление, которое снова сломается на следующей машине. Команды из этого руководства — те, что нужно вставить в тикет.

Похожие статьи