Naprawa ProxyError, SSLError i ConnectTimeout w Requests

requests.exceptions.ProxyError to objaw, a nie przyczyna. Oto zdekodowana hierarchia wyjątków, każdy traceback dopasowany do rzeczywistej naprawy oraz funkcja sprawdzania kondycji, która klasyfikuje awarie i rotuje wokół martwych wyjść.

requests.exceptions.ProxyError to jedna z najmniej pomocnych wiadomości o błędach w Pythonie: występuje w przypadku martwego proxy, błędnych danych uwierzytelniających, złego schematu, przeciążonego wyjścia i blokady zapory, wszystkie z niemal identycznymi tracebackami. Sztuczka, aby szybko to naprawić, polega na zrozumieniu, że ProxyError nie jest przyczyną pierwotną — to kategoria. W źródle Requests, ProxyError, SSLError i ConnectTimeout są podklasami ConnectionError, a każdy z nich występuje na określonym etapie cyklu życia żądania. Przeczytaj etap, a poznasz przyczynę. Ten przewodnik dekoduje hierarchię wyjątków, dopasowuje każdy typowy traceback do rzeczywistej naprawy i dostarcza funkcję sprawdzania kondycji, która automatycznie klasyfikuje awarie i rotuje wokół umierających wyjść.

Hierarchia wyjątków w requests

Każdy błąd związany z proxy w Requests pochodzi od RequestException. Przydatną gałęzią do debugowania jest ConnectionError, ponieważ trzy wyjątki, które faktycznie napotykasz, znajdują się pod nią:

Ponieważ pierwsze trzy dzielą rodzica, jedno except requests.exceptions.ConnectionError łapie je wszystkie dla logiki ponawiania — podczas gdy łapanie podklas indywidualnie pozwala na logowanie dlaczego każdy z nich zawiódł. Czysta konfiguracja, która unika większości z nich, jest omówiona w naszym przewodniku po proxy w Python Requests; ten post dotyczy tego, co zrobić, gdy traceback jest już na ekranie.

Jedna nawyk oszczędza więcej czasu niż jakakolwiek pojedyncza naprawa: czytaj traceback od dołu do góry. Requests opakowuje podstawową awarię urllib3, więc górne ramki opisują gdzie dokonano wywołania, a dolne ramki opisują co poszło nie tak. Linia, której szukasz, to najgłębsza klauzula Caused by — nazywa ona konkretną awarię (odmowa połączenia, niezgodność certyfikatu, błąd parsowania portu), którą zewnętrzny ProxyError lub ConnectionError jedynie ponownie podnosi. Gdy już potrafisz przeczytać tę linię, reszta tego przewodnika to tabela wyszukiwania.

Klasyczny ValueError przed ProxyError

Najczęściej wyszukiwany traceback proxy to nawet nie ProxyError — to ValueError: invalid literal for int() with base 10, rzucany głęboko wewnątrz urllib3. Dzieje się tak, gdy osadzasz dane uwierzytelniające w wartości proxy bez schematu, więc parser odczytuje tekst po dwukropku jako numer portu:

# Broken — no scheme, so 'pass@host' is parsed as host:port
proxies = {"https": "user:pass@45.11.22.33:8000"}
# -> ValueError: invalid literal for int() with base 10: 'pass@45.11.22.33'

# Fixed — scheme in front, password URL-encoded if it has @ : or /
from urllib.parse import quote
pw = quote("p@ss:word", safe="")
proxies = {
    "http":  f"http://user:{pw}@gate.quantumproxies.io:PORT",
    "https": f"http://user:{pw}@gate.quantumproxies.io:PORT",
}
Diagram przepływu pokazujący, gdzie każdy wyjątek w requests w Pythonie występuje w cyklu życia żądania: MissingSchema, ProxyError, SSLError i ReadTimeout
Nazwa etapu cyklu życia wskazuje przyczynę: literówka w czasie budowy to MissingSchema, nieudane połączenie proxy to ProxyError, awaria TLS to SSLError, a wolne wyjście to ReadTimeout.

Sprawdzanie kondycji proxy, które klasyfikuje awarie

Zamiast zgadywać, złap każdy typ wyjątku i zamień go na werdykt w prostym języku. Ta funkcja zwraca adres IP wyjścia w przypadku sukcesu i oznaczoną przyczynę w przypadku niepowodzenia — umieść ją przed każdym skrobaniem, aby potwierdzić, że proxy jest aktywne, zanim zużyjesz na nim żądania. Działa z dowolną uwierzytelnioną bramą, w tym proxy rezydencjalnymi:

import requests

def check_proxy(proxies, url="https://httpbin.org/ip", timeout=(5, 20)):
    try:
        r = requests.get(url, proxies=proxies, timeout=timeout)
        r.raise_for_status()
        return True, r.json().get("origin")
    except requests.exceptions.ProxyError as e:
        return False, f"proxy unreachable or auth rejected: {e}"
    except requests.exceptions.SSLError as e:
        return False, f"TLS failed (https:// in the https key?): {e}"
    except requests.exceptions.ConnectTimeout:
        return False, "proxy did not answer within the connect window"
    except requests.exceptions.ReadTimeout:
        return False, "target too slow after connect (exit quality)"
    except requests.exceptions.RequestException as e:
        return False, f"other request error: {e}"

ok, detail = check_proxy(proxies)
print("OK" if ok else "FAIL", detail)

Gdy curl działa, ale Python wyrzuca ProxyError

Jeśli te same dane uwierzytelniające działają w curl, ale wywołują ProxyError w Pythonie, zmienna środowiskowa prawie zawsze nadpisuje twój słownik. Requests odczytuje HTTP_PROXY, HTTPS_PROXY i NO_PROXY z powłoki, a nieaktualna wartość korporacyjna cicho przekierowuje każde wywołanie. Wydrukuj session.proxies, aby zobaczyć, co jest naprawdę używane, a następnie całkowicie wyłącz wyszukiwanie środowiska:

import requests

session = requests.Session()
session.trust_env = False          # ignore HTTP_PROXY / HTTPS_PROXY from the shell
session.proxies = {
    "http":  "http://USER:PASS@gate.quantumproxies.io:PORT",
    "https": "http://USER:PASS@gate.quantumproxies.io:PORT",
}
print(session.get("https://httpbin.org/ip", timeout=(5, 20)).json())

407, który przetrwa poprawne dane uwierzytelniające, wskazuje na plan białej listy IP wywoływany z niezarejestrowanego adresu — pełna lista przyczyn znajduje się w naszym przewodniku 407 Proxy Authentication Required.

Przerywany ProxyError: rotuj, nie restartuj

Frustrujący przypadek to kod, który działa przez dwadzieścia minut, potem wyrzuca ProxyError, a następnie znowu działa. To nie jest błąd w twoim skrypcie — to pojedynczy adres IP wyjścia umiera lub jest ograniczany w trakcie działania. Naprawą jest ponowienie z rotacją: opakuj wywołanie, złap ConnectionError, i pozwól, aby rotująca brama dostarczyła ci nowy adres IP przy następnym podejściu. Poprzez rotujące proxy każde ponowienie przechodzi przez inne wyjście, więc jeden martwy adres nigdy nie może spowodować niepowodzenia żądania dwa razy:

import requests

def get_with_rotation(url, proxies, attempts=4):
    last = None
    for _ in range(attempts):
        try:
            r = requests.get(url, proxies=proxies, timeout=(5, 20))
            if r.status_code not in (429, 500, 502, 503, 504):
                return r
            last = r.status_code
        except requests.exceptions.ConnectionError as e:  # Proxy/SSL/ConnectTimeout
            last = e
    raise RuntimeError(f"failed after {attempts} attempts: {last}")

Jeśli ProxyError utrzymuje się na wielu nowych adresach IP, problem przeniósł się z twojej puli na cel: jesteś blokowany, a nie rozłączany. To inna walka — zobacz listę kontrolną antybanową dla tempa, nagłówków i higieny sesji.

Jest jeszcze jedno rozróżnienie warte zapamiętania, ponieważ zmienia sposób, w jaki reagujesz. ProxyError lub ConnectTimeout oznacza, że żądanie nigdy nie zostało zakończone, więc ponowienie jest bezpieczne nawet dla POST — nic się nie wydarzyło po drugiej stronie. ReadTimeout, przeciwnie, oznacza, że cel otrzymał twoje żądanie i po prostu zbyt długo odpowiadał; ponowienie nie-idempotentnego zapisu może spowodować podwójne przesłanie. Kiedy budujesz pętlę ponawiania, traktuj awarie na etapie połączenia jako swobodnie ponawialne, a awarie na etapie odczytu jako ponawialne tylko dla GET i HEAD. Ta jedna zasada zapobiega subtelnemu błędowi, w którym niestabilne proxy zamienia jedno zamówienie na trzy.

Lista kontrolna dopasowująca typowe wiadomości o błędach proxy w requests w Pythonie do ich jednolinijkowych napraw, od nie można połączyć się z proxy po przerywane awarie
Przeczytaj wiadomość, zastosuj naprawę: zły host, brakujący schemat, zły klucz TLS, niezakodowane hasło lub umierające wyjście, każdy z nich mapuje się na jeden krok naprawczy.

Często zadawane pytania

Co powoduje requests.exceptions.ProxyError: nie można połączyć się z proxy?

Host proxy lub port jest błędny, proxy jest wyłączone lub zapora blokuje połączenie, zanim jakiekolwiek żądanie opuści. Zweryfikuj punkt końcowy za pomocą curl -x używając tych samych danych uwierzytelniających; jeśli curl również zawiedzie, proxy jest nieosiągalne, a jeśli curl się powiedzie, winowajcą jest zmienna środowiskowa lub nieprawidłowy słownik w twoim Pythonie.

Dlaczego ProxyError jest opakowany w HTTPSConnectionPool?

To opakowanie po prostu nazywa pulę połączeń, której urllib3 użyło do dotarcia do celu — to szum wokół rzeczywistej wiadomości zagnieżdżonej wewnątrz. Przeczytaj najgłębszą klauzulę Caused by: Cannot connect to proxy oznacza awarię połączenia, podczas gdy wiadomość 407 lub SSL wewnątrz tej samej puli wskazuje na problem z uwierzytelnieniem lub TLS/schematem.

Jak zatrzymać requests przed odczytywaniem proxy ze środowiska?

Ustaw session.trust_env = False na swojej Sesji, lub przekaż trust_env=False równoważnie, aby Requests ignorowało HTTP_PROXY i HTTPS_PROXY. To jest naprawa, gdy proxy działa w jednej powłoce, ale wyrzuca ProxyError w innej, lub gdy zmienna korporacyjna przejmuje skrobak, którego nie skonfigurowałeś do używania proxy.

Czy ConnectTimeout jest bezpieczny do ponowienia?

Tak — dokumentacja Requests oznacza ConnectTimeout jako bezpieczny do ponowienia, ponieważ żądanie nigdy nie dotarło do serwera, więc nie mogło wystąpić żadne działanie uboczne. Ponów je, najlepiej przez rotującą bramę, aby następna próba użyła innego, szybszego wyjścia. ReadTimeout jest bardziej ryzykowny do ponowienia w ciemno na nie-idempotentnych metodach jak POST.

Gdy przestaniesz traktować ProxyError jako pojedynczą usterkę i zaczniesz czytać go według etapu cyklu życia, naprawy stają się mechaniczne: literówki w schemacie pojawiają się przed siecią, awarie połączeń nazywają martwe proxy, błędy SSL nazywają zły klucz, a przerywane awarie chcą rotacji, a nie restartu. Czyste wyjścia eliminują większość z nich całkowicie.

Skrob na rezydencjalnych IP, które pozostają aktywne