プライベートAPIでRequestsのProxyError?まず環境を確認する
Requestsは環境からプロキシ、認証、CAの設定をマージします。プロセスが見ているものを読み、プライベートホストを除外し、TLS検証は有効のままにします。
VPNは上がっており、社内ダッシュボードはブラウザで開くのに、Pythonスクリプトは依然としてプロキシエラーを出します。その状況でトンネルに問題はありません:インタプリタが自身の環境からプロキシアドレスを読み、そこへリクエストを送っているのです。この手順書は、プロセスが実際に見ている環境を読み、最短の除外を適用し、その後で初めて明示的なセッションポリシーを検討します。各手順は、出力が続行可否を決めるコマンドで終わります。
プロセスが実際に見ている環境を読む
デスクトップのプロキシ設定、シェルのエクスポート、systemdユニットのEnvironment=行は三つの異なるソースであり、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の解決先を変えるからです。出力が空なら環境は問題ではなく、この節は読むのをやめてかまいません。見覚えのないプロキシが並んでいるなら、何かを変える前に、それを設定しているファイルやユニットを探してください。
特定のURLに対してRequestsが何をするかを尋ねます。この呼び出しはパケットを送らずに、そのリクエストのプロキシ、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に影響しなくなり、それは同梱の証明書ストアにフォールバックします。組織が独自のCAでTLSを終端している場合、そのフォールバックは今行った変更とは無関係に見える検証エラーを出します。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で検証されること、リクエストが期待どおりのステータスを返すこと、変更が再現可能な場所にコミットされていることです。説明のつかない環境のまま緑になったスクリプトは、次のマシンでまた壊れる修正です。この手順書のコマンドこそ、チケットに貼るべきものです。