ProxynetProxynet

خطای Max Retries Exceeded With URL چیست و چگونه رفع می‌شود؟

تاریخ انتشار:

16 دقیقه مطالعه

Acar Diveroli
نویسنده: Acar Diveroli
مسیر به جعبه آبی پروکسی می‌رسد، اما تونل به سوی مقصد کم‌رنگ با 403 قرمز قطع می‌شود؛ در نوار پایین ⁦RETRIES 0⁩ نوشته شده است.
فهرست مطالب

اسکریپت رصد قیمت شما تمام شب بی‌مشکل کار کرد. امروز صبح آدرس پروکسی پلن تازه‌تان را در آن گذاشتید و حالا ترمینال یک سطر طولانی چاپ می‌کند: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url. اسکریپت هیچ تنظیمی برای تلاش دوباره ندارد و میزبانی که در پیام آمده همان فروشگاهی است که از آن داده جمع می‌کنید، پس نگاهتان مستقیم به فروشگاه می‌رود. اما در انتهای همان سطر، داخل پرانتز، نوشته شده Tunnel connection failed: 403 Forbidden و این پاسخ را پروکسی داده است، نه فروشگاه.

در این راهنما پیام را جزءبه‌جزء باز می‌کنیم: چرا با اینکه هیچ تلاش دوباره‌ای تنظیم نکرده‌اید «max retries» می‌گوید، جدولی برای خواندن پیام که از رشته‌های خطای تولیدشده به دست خودمان ساخته شده، گونه‌های مربوط به پروکسی، مهلت‌ها (timeout) و همین خطا در pip. در پایان یک اسکریپت آزموده Python آمده که علت را نام می‌برد.

«Max retries exceeded with url» یعنی چه؟

این پیام را urllib3 تولید می‌کند، کتابخانه‌ای که اتصال‌ها را برای Requests باز می‌کند. وقتی urllib3 از یک درخواست دست می‌کشد، MaxRetryError را پرتاب می‌کند. Requests آن را می‌گیرد و استثنای خودش را پرتاب می‌کند که بر اساس علت درونی انتخاب می‌شود: ConnectTimeout، ProxyError، SSLError، RetryError وقتی تلاش‌های دوباره بر اساس کد وضعیت تمام شود، یا ConnectionError ساده برای بقیه موارد. این رشته‌ای از اجرای آزمایشی ماست که به چهار بخش تقسیم شده است:

text
requests.exceptions.ProxyError                        <- 1. the Requests exception
HTTPSConnectionPool(host='example.com', port=443):    <- 2. the pool: target host and port
Max retries exceeded with url: /                      <- 3. the wrapper text and the path
(Caused by ProxyError('Unable to connect to proxy',   <- 4. the real cause, outermost first
    OSError('Tunnel connection failed: 403 Forbidden')))

بیشتر اشتباه‌ها در بخش 2 رخ می‌دهد. برای آدرس https://، سطر pool مقصد را نشان می‌دهد، حتی وقتی درخواست از پروکسی می‌گذرد؛ آدرس پروکسی فقط داخل بخش 4 می‌آید، آن هم اگر بیاید. برای آدرس ساده http:// که از پروکسی فرستاده می‌شود، وضع برعکس است: سطر pool پروکسی را نشان می‌دهد و بخش 3 آدرس کامل مقصد را در خود دارد، مانند HTTPConnectionPool(host='127.0.0.1', port=8083): Max retries exceeded with url: http://example.com/.

چرا وقتی هیچ تلاش دوباره‌ای تنظیم نکرده‌اید «max retries» می‌گوید؟

Requests به‌طور پیش‌فرض اتصال‌های ناموفق را دوباره تلاش نمی‌کند. در کد منبع requests/adapters.py مقدار DEFAULT_RETRIES = 0 است و adapter آن را به Retry(0, read=False) تبدیل می‌کند. مستندات urllib3 برای کلاس Retry می‌گوید خطاها درون MaxRetryError پیچیده می‌شوند، مگر آنکه تلاش دوباره با retries=False غیرفعال شده باشد. صفر تلاش دوباره غیرفعال به حساب نمی‌آید، پس همان یک تلاش ناموفق هم به شکل «Max retries exceeded» بیرون می‌آید.

بالا بردن تعداد تلاش‌های دوباره علت را برطرف نمی‌کند. نام پروکسی غلط‌نوشته‌شده، پورت نادرست یا تونل ردشده هر بار به همان شکل شکست می‌خورد. در آزمون ما، یک آدرس دسترس‌ناپذیر با مهلت اتصال 2 ثانیه در حالت پیش‌فرض پس از 2 ثانیه شکست خورد و با دو تلاش دوباره و backoff کوتاه پس از 7 ثانیه. تلاش دوباره فقط در قطعی‌های کوتاه شبکه کمک می‌کند.

پیام خطا را گام‌به‌گام چگونه بخوانیم؟

پیام را از بیرون به درون بخوانید:

  1. استثنای Requests را پیدا کنید. ProxyError یعنی شکست در مسیر رسیدن به پروکسی یا در خود پروکسی رخ داده است.
  2. سطر pool را بخوانید. برای مقصدهای https://، مقدارهای host و port مربوط به مقصدند. پورت 443 از طریق پروکسی HTTP یعنی درخواست از یک تونل CONNECT می‌گذرد.
  3. نخستین کلاس پس از Caused by را بخوانید. این تشخیص urllib3 است: NameResolutionError، NewConnectionError، ConnectTimeoutError، SSLError یا ProxyError.
  4. درونی‌ترین متن را بخوانید. Failed to resolve 'name'، [Errno 111] Connection refused در Linux، [WinError 10061] در Windows، CERTIFICATE_VERIFY_FAILED یا Tunnel connection failed: 403 Forbidden. مقدار host= در این بخش ممکن است آدرس پروکسی باشد.
  5. به مدت فراخوانی توجه کنید. شکستی که دقیقاً پس از مهلت اتصال شما (یا مضربی از آن) رخ دهد timeout است؛ شکست سریع‌تر یعنی اتصال رد شده یا تونل پذیرفته نشده است. در آزمون‌های ما روی Windows، اتصال ردشده برای هر آدرس حدود دو ثانیه طول کشید و localhost چهار ثانیه (نخست IPv6، سپس IPv4).

خطاهای HTTPSConnectionPool: بخش Caused by چه می‌گوید

همه رشته‌های زیر را روی ⁦Python 3.13⁩ با ⁦Requests 2.34.2⁩ و ⁦urllib3 2.8.0⁩ و از طریق یک پروکسی آزمایشی محلی تولید کرده‌ایم. بخش‌های طولانی با ... کوتاه شده‌اند؛ متن خطای Windows به زبان سیستم هم بستگی دارد.

Caused by (درونی‌ترین بخش)استثنای Requestsچه رخ دادنخست بررسی کنید
NameResolutionError(... Failed to resolve 'no-such-host.invalid' ...)ConnectionErrorنام مقصد به IP تبدیل نشداملای URL، DNS، VPN
NewConnectionError(... [Errno 111] or [WinError 10061] ...)ConnectionErrorهیچ برنامه‌ای روی آن پورت گوش نمی‌دهدروشن بودن سرویس، پورت درست
ConnectTimeoutError(... 'Connection to 10.255.255.1 timed out. (connect timeout=2)')ConnectTimeoutاتصال TCP در مهلت تعیین‌شده برقرار نشدآدرس، فایروال، مقدار مهلت
SSLError(SSLCertVerificationError(... CERTIFICATE_VERIFY_FAILED ...))SSLErrorگواهی مورد اعتماد نیستcertifi، گواهی ریشه سازمان
ProxyError('Unable to connect to proxy', NewConnectionError(...))ProxyErrorپورت پروکسی اتصال را رد کردمیزبان و پورت پروکسی، فایروال محلی
ProxyError('Unable to connect to proxy', NameResolutionError(... 'pr.proxynet.invalid' ...))ProxyErrorنام پروکسی به IP تبدیل نشدغلط املایی در میزبان پروکسی
ProxyError('Unable to connect to proxy', ConnectTimeoutError(...))ProxyErrorپروکسی به‌موقع پاسخ ندادآدرس پروکسی، قواعد خروجی برای آن پورت
ProxyError(..., OSError('Tunnel connection failed: 403 Forbidden'))ProxyErrorپروکسی تونل CONNECT را رد کردپورت مقصد، نوع و پورت پروکسی، قواعد مجاز
ProxyError(..., OSError('Tunnel connection failed: 407 Proxy Authentication Required'))ProxyErrorپروکسی اطلاعات ورود معتبر می‌خواهداحراز هویت پروکسی را ببینید
ProxyError(..., OSError('Tunnel connection failed: 502 Bad Gateway'))ProxyErrorپروکسی به مقصد نرسیدمیزبان و پورت مقصد
ProxyError('... Your proxy appears to only use HTTP and not HTTPS ...', SSLError(... WRONG_VERSION_NUMBER ...))ProxyErrorآدرس پروکسی با https:// شروع می‌شودhttp:// بنویسید

سه نکته بیرون از جدول می‌ماند. پروکسی‌ای که مهلتش تمام شود ProxyError پرتاب می‌کند، پس except ConnectTimeout هرگز آن را نمی‌بیند. تمام شدن مهلت خواندن پیچیده نمی‌شود: به شکل ReadTimeout با متن Read timed out. (read timeout=3) می‌رسد. و آدرس ساده http:// از طریق پروکسی با رمز نادرست هیچ استثنایی پرتاب نمی‌کند؛ پاسخی با کد وضعیت 407 می‌گیرید.

«ProxyError: Cannot connect to proxy» یعنی چه؟

Cannot connect to proxy. (⁦urllib3 1.26⁩) و Unable to connect to proxy (⁦urllib3 2.x⁩) یک خطا هستند. هر دو ممکن است روی یک رایانه دیده شوند: ⁦pip 25.x⁩ نسخه ⁦urllib3 1.26.20⁩ را همراه خود دارد، در حالی که نصب تازه Requests نسخه ⁦urllib3 2.8.0⁩ را می‌آورد. به‌جای متن پیام، آرگومان دوم را بخوانید:

  • NewConnectionError: هیچ برنامه‌ای روی آن پورت پروکسی گوش نمی‌دهد یا فایروال محلی جلوی آن را می‌گیرد. میزبان و پورت را دوباره از پنل کپی کنید.
  • NameResolutionError: نام میزبان پروکسی غلط نوشته شده یا DNS شما نمی‌تواند آن را به IP تبدیل کند.
  • ConnectionResetError (Errno 104 در Linux و WinError 10054 در Windows): پروکسی یا دستگاهی در مسیر اتصال را بست.
  • OSError('Tunnel connection failed: ...'): پروکسی پاسخ داد اما تونل را باز نکرد؛ کد وضعیت همان پاسخ اوست.

برای نسخه مرورگری این خطا، یعنی «The proxy server is not responding»، نوشته خطای پروکسی چیست؟ را ببینید.

خطای ⁦Tunnel connection failed: 403 Forbidden⁩: چرا پروکسی CONNECT را رد می‌کند؟

درخواست https:// از طریق پروکسی HTTP با CONNECT example.com:443 آغاز می‌شود. پروکسی یک اتصال TCP به مقصد باز می‌کند و بایت‌های رمزگذاری‌شده را جابه‌جا می‌کند؛ خود صفحه را هرگز نمی‌بیند. بر اساس بخش 9.3.6 از ⁦RFC 9110⁩، هر پاسخی جز 2xx یعنی تونل برقرار نشده است. ماژول http.client در Python این پاسخ را می‌خواند و Tunnel connection failed می‌نویسد.

403 تصمیم خود پروکسی است؛ مقصد هرگز درخواست شما را ندیده است. این RFC می‌گوید پروکسی‌ها باید CONNECT را به پورت‌های شناخته‌شده یا فهرستی از مقصدهای امن محدود کنند، چون تونلی به پورت 25 می‌تواند هرزنامه را جابه‌جا کند. علت‌های رایج:

  • پورت مقصد مجاز نیست. ممکن است https://example.com:8443/ شکست بخورد در حالی که https://example.com/ کار می‌کند.
  • فهرست مجاز. پروکسی‌های سازمانی و برخی پلتفرم‌های میزبانی فقط به دامنه‌های تأییدشده اجازه می‌دهند. از تیم IT بپرسید یا مستندات پلتفرم را بخوانید؛ سیاست را دور نزنید.
  • نوع یا پورت نادرست پروکسی، برای نمونه پورتی که برای پروتکل دیگری در نظر گرفته شده است.
  • قواعد ارائه‌دهنده. ارائه‌دهنده می‌تواند تونل به برخی مقصدها را رد کند؛ مستندات آن می‌گوید از کدام کدهای وضعیت استفاده می‌کند.

403 از سوی سایت مقصد به شکل پاسخی عادی همراه یک صفحه می‌رسد؛ این حالت را در کدهای وضعیت HTTP در وب اسکرپینگ توضیح داده‌ایم. 407 در تونل یعنی پروکسی اطلاعات ورود می‌خواهد.

آدرس پروکسی باید با http:// شروع شود یا https://؟

در دیکشنری proxies، کلید طرح (scheme) آدرس مقصد است و مقدار، راه رسیدن به پروکسی. نمونه خود مستندات Requests کلید 'https' را به 'http://10.10.1.10:1080' نگاشت می‌کند. بیشتر پروکسی‌ها HTTP ساده صحبت می‌کنند و برای سایت‌های رمزگذاری‌شده تونل CONNECT باز می‌کنند (صفحه پروکسی HTTPS ما را ببینید)، پس هر دو کلید مقدار http:// می‌گیرند:

python
proxies = {
    "http": "http://user:pass@pr.proxynet.io:8000",
    "https": "http://user:pass@pr.proxynet.io:8000",  # http:// here too
}

اگر مقدار با https:// شروع شود، ⁦urllib3 2.x⁩ با خود پروکسی اتصال TLS باز می‌کند، پروکسی HTTP ساده با بایت‌های غیر TLS پاسخ می‌دهد و خطای WRONG_VERSION_NUMBER را همراه راهنمایی Your proxy appears to only use HTTP and not HTTPS می‌گیرید. صفحه urllib3 درباره این خطا همین راه‌حل را می‌دهد، برای متغیر نادرست HTTPS_PROXY هم.

دو نکته مرتبط:

  • SOCKS. بسته requests[socks] را نصب کنید. با socks5:// رایانه خود شما نام را به IP تبدیل می‌کند، پس در آزمون ما نام نادرست پیش از تماس با پروکسی شکست خورد؛ با socks5h:// این کار را پروکسی انجام می‌دهد (تفاوت پروکسی SOCKS و HTTP، پروکسی SOCKS5).
  • متغیرهای محیطی. صفحه کاربرد پیشرفته Requests هشدار می‌دهد که پروکسی‌های تعریف‌شده در متغیرهای محیطی می‌توانند session.proxies را بازنویسی کنند و توصیه می‌کند proxies= را در هر درخواست بدهید. این متغیرها را در استفاده از پروکسی در wget توضیح داده‌ایم.

بدون مهلت چه اتفاقی می‌افتد؟ ConnectTimeout در برابر ReadTimeout

Requests مهلت پیش‌فرض ندارد: بدون timeout= ممکن است یک فراخوانی چند دقیقه معلق بماند و هیچ خطایی برای خواندن به شما ندهد. یک تاپل مانند timeout=(3.05, 20) بدهید: نخست مهلت اتصال، سپس مهلت خواندن، بر حسب ثانیه. مستندات پیشنهاد می‌کند مهلت اتصال کمی بیشتر از مضربی از 3 باشد، یعنی پنجره پیش‌فرض ارسال دوباره بسته‌ها در TCP.

این دو مهلت به دو شکل متفاوت شکست می‌خورند:

  • ConnectTimeout: اتصال TCP به‌موقع برقرار نشد. این خطا درون «Max retries exceeded» پیچیده می‌رسد و مرجع API آن را برای تلاش دوباره امن می‌داند.
  • ReadTimeout: اتصال برقرار شد اما در مهلت خواندن داده‌ای نرسید. این خطا پیچیده نمی‌شود و ممکن است سرور درخواست را پیش‌تر پردازش کرده باشد، پس تلاش دوباره برای یک POST ممکن است کار را دو بار انجام دهد.

مهلت اتصال برای هر آدرس IP جداگانه اعمال می‌شود، پس نامی که آدرس IPv4 و IPv6 دارد می‌تواند زمان انتظار را دو برابر کند. مهلت خواندن فاصله میان دو بایت پیاپی است، نه سقفی برای کل دانلود. مقادیر پیش‌فرض کتابخانه‌های دیگر در مقایسه HTTPX، Requests و AIOHTTP آمده است.

همین خطا در pip: «Retrying ... after connection broken by»

pip نسخه‌های خودش از Requests و urllib3 را همراه دارد، پس pip install به همان دلایل شکست می‌خورد. مستندات pip مقادیر پیش‌فرض را چنین آورده است: --retries 5 و --timeout 15 ثانیه. ما ⁦pip 26.2.1⁩ را با --retries 2 از طریق پروکسی محلی‌ای اجرا کردیم که هر CONNECT را با 403 رد می‌کند:

text
WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'OSError('Tunnel connection failed: 403 Forbidden')': /simple/six/
WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'OSError('Tunnel connection failed: 403 Forbidden')': /simple/six/
ERROR: Could not find a version that satisfies the requirement six (from versions: none)
ERROR: No matching distribution found for six

بسته وجود دارد؛ pip هرگز به ایندکس نرسید. علت در سطرهای WARNING است. ⁦pip 25.2⁩ همین هشدار را با عبارت‌بندی ⁦urllib3 1.26⁩ یعنی ProxyError('Cannot connect to proxy.', ...) چاپ می‌کرد.

برای CERTIFICATE_VERIFY_FAILED پشت پروکسی سازمانی که TLS را بازرسی می‌کند، گواهی ریشه سازمان را با --cert بدهید؛ --trusted-host تأیید گواهی را خاموش می‌کند و آخرین راه است. تنظیم پروکسی برای pip را در تنظیمات پروکسی در Linux توضیح داده‌ایم.

اسکریپت Python که علت را نام می‌برد

این اسکریپت یک استثنا را به یک سطر تبدیل می‌کند: چه چیزی شکست خورد و چه چیزی را باید بررسی کرد. این اسکریپت:

  • فقط اتصال‌های ناموفق را، آن هم دو بار، دوباره تلاش می‌کند؛
  • مقدار read=False را نگه می‌دارد: در آزمون ما read=0 یک ReadTimeout را به ConnectionError پیچیده تبدیل کرد؛
  • مقدار other=0 را تنظیم می‌کند: بدون آن، تونلی که با 403 یا 407 رد شده بود سه بار تلاش شد؛
  • در هر درخواست proxies= و timeout=(3.05, 20) را می‌دهد؛
  • کلاس‌های ProxyError، SSLError و ConnectTimeout را پیش از ConnectionError که کلاس والد آن‌هاست می‌گیرد.

تلاش دوباره بر اساس کد وضعیت لایه جداگانه‌ای است (کدهای وضعیت HTTP در وب اسکرپینگ) و چرخش پروکسی هم همین‌طور (چرخاندن پروکسی در Python).

python
"""Find the real cause behind "Max retries exceeded with url" and say what to check."""
import re
import ssl
import time

import requests
from requests.adapters import HTTPAdapter
from urllib3.exceptions import (
    ConnectTimeoutError,
    NameResolutionError,
    NewConnectionError,
    ProxyError as Urllib3ProxyError,
)
from urllib3.util import Retry

PROXY = "http://user:pass@pr.proxynet.io:8000"  # http:// even for https:// targets
TIMEOUT = (3.05, 20)  # seconds: (connect, read)


def make_session():
    """A session that retries failed connections twice and nothing else."""
    retry = Retry(
        total=2,
        connect=2,      # DNS failures, refused and timed-out connections
        read=False,     # keep ReadTimeout a ReadTimeout: the server may have the request
        other=0,        # a proxy that refused the tunnel will refuse it again
        status=0,       # retries by status code belong to another layer
        backoff_factor=0.5,
    )
    adapter = HTTPAdapter(max_retries=retry)
    session = requests.Session()
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    return session


def causes(exc):
    """The exception and every error wrapped inside it, outermost first."""
    chain = []
    while exc is not None and all(exc is not seen for seen in chain):
        chain.append(exc)
        inner = None
        for candidate in (getattr(exc, "reason", None), getattr(exc, "original_error", None),
                          exc.__cause__, exc.__context__, *exc.args[:2]):
            if isinstance(candidate, BaseException):
                inner = candidate
                break
        exc = inner
    return chain


def explain(exc):
    """One line: what failed, on which side, and what to check first."""
    chain = causes(exc)
    text = " | ".join(str(e) for e in chain)
    side = "proxy" if any(isinstance(e, Urllib3ProxyError) for e in chain) else "target"

    tunnel = re.search(r"Tunnel connection failed: (\d{3})", text)
    if tunnel:
        code = tunnel.group(1)
        if code == "407":
            return "proxy asked for credentials (407): check user:pass or the IP whitelist"
        if code == "403":
            return "proxy refused the CONNECT tunnel (403): check the target port and the proxy's allow rules"
        return f"proxy was reached but could not reach the target ({code}): check the target host and port"
    if "appears to only use HTTP" in text:
        return "proxy URL starts with https:// but the proxy speaks plain HTTP: write http://"
    if any(isinstance(e, NameResolutionError) for e in chain):
        host = re.search(r"Failed to resolve '([^']+)'", text)
        return f"{side} name {host.group(1) if host else ''} does not resolve: check spelling, DNS and VPN"
    if any(isinstance(e, ssl.SSLCertVerificationError) for e in chain):
        return "certificate not trusted: update certifi or set REQUESTS_CA_BUNDLE to your company root CA"
    if isinstance(exc, requests.exceptions.ReadTimeout):
        return "connected, but no answer within the read timeout: the server may have the request"
    if any(isinstance(e, NewConnectionError) for e in chain):
        return f"{side} refused the connection: wrong port, service down, or a firewall rejects it"
    if any(isinstance(e, ConnectTimeoutError) for e in chain):
        return f"no TCP connection to the {side} within the connect timeout: check address, port, firewall"
    return f"unrecognised, read the innermost error: {chain[-1]!r}"


def fetch(session, url, proxy=PROXY):
    """GET one URL and print the status, or the diagnosis if the request failed."""
    proxies = {"http": proxy, "https": proxy} if proxy else None  # per request: env vars cannot override it
    start = time.monotonic()
    try:
        resp = session.get(url, proxies=proxies, timeout=TIMEOUT)
    except requests.exceptions.ProxyError as exc:      # before ConnectionError: it is a subclass
        kind, error = "ProxyError", exc
    except requests.exceptions.SSLError as exc:        # also a ConnectionError
        kind, error = "SSLError", exc
    except requests.exceptions.ConnectTimeout as exc:  # a ConnectionError and a Timeout at once
        kind, error = "ConnectTimeout", exc
    except requests.exceptions.ReadTimeout as exc:     # never wrapped in "Max retries exceeded"
        kind, error = "ReadTimeout", exc
    except requests.exceptions.ConnectionError as exc:
        kind, error = "ConnectionError", exc
    else:
        print(f"{'OK':<16}{time.monotonic() - start:6.2f}s  {url}  HTTP {resp.status_code}")
        return resp
    print(f"{kind:<16}{time.monotonic() - start:6.2f}s  {url}\n{'':<24}{explain(error)}")
    return None


if __name__ == "__main__":
    session = make_session()
    for url in ["https://httpbin.org/ip", "https://example.com/"]:
        fetch(session, url)

در ⁦urllib3 2.x⁩، NameResolutionError زیرکلاس NewConnectionError است و آن هم زیرکلاس ConnectTimeoutError، پس explain() نخست خاص‌ترین کلاس را آزمایش می‌کند. اسکریپت فقط به pip install requests نیاز دارد.

خروجی چه شکلی دارد

تابع fetch() را روی ⁦Windows 11⁩ برای هر حالت یک بار اجرا کردیم: یک پروکسی آزمایشی محلی (رمز درست و نادرست، پورت بسته، نام غلط‌نوشته‌شده، طرح https://)، پروکسی دومی که هر CONNECT را رد می‌کند، و میزبان‌های واقعی برای حالت‌های گواهی و مهلت. مهلت خواندن 5 ثانیه بود:

text
OK                0.77s  https://httpbin.org/ip  HTTP 200
ProxyError        0.02s  https://httpbin.org/ip
                        proxy asked for credentials (407): check user:pass or the IP whitelist
ProxyError        0.00s  https://example.com/
                        proxy refused the CONNECT tunnel (403): check the target port and the proxy's allow rules
ProxyError        0.02s  https://no-such-host.invalid/
                        proxy was reached but could not reach the target (502): check the target host and port
ProxyError        7.10s  https://httpbin.org/ip
                        proxy refused the connection: wrong port, service down, or a firewall rejects it
ProxyError        1.02s  https://httpbin.org/ip
                        proxy name pr.proxynet.invalid does not resolve: check spelling, DNS and VPN
ProxyError        0.21s  https://httpbin.org/ip
                        proxy URL starts with https:// but the proxy speaks plain HTTP: write http://
SSLError          0.63s  https://self-signed.badssl.com/
                        certificate not trusted: update certifi or set REQUESTS_CA_BUNDLE to your company root CA
ConnectTimeout   10.16s  http://10.255.255.1/
                        no TCP connection to the target within the connect timeout: check address, port, firewall
ReadTimeout       5.02s  http://127.0.0.1:8082/
                        connected, but no answer within the read timeout: the server may have the request
ConnectionError  13.12s  http://localhost:8083/
                        target refused the connection: wrong port, service down, or a firewall rejects it

تونل‌های ردشده در چند میلی‌ثانیه شکست خوردند، چون other=0 جلوی تکرارشان را گرفت. پورت بسته پروکسی سه تلاش حدوداً دوثانیه‌ای به‌اضافه یک ثانیه backoff طول کشید، مهلت اتصال سه بار 3.05 ثانیه به‌اضافه backoff و localhost 13 ثانیه، چون هر تلاش نخست IPv6 و سپس IPv4 را امتحان کرد. 502 از پروکسی آزمایشی ما آمد که نمی‌توانست نام مقصد را به IP تبدیل کند.

کجا با این خطا روبه‌رو می‌شوید

  • اسکرپینگ از طریق پروکسی: اشتباه‌های پروکسی پیش از هر کد وضعیتی همین‌جا ظاهر می‌شوند (استخراج داده).
  • بیلدهای CI و Docker پشت پروکسی سازمانی: بیلدها اغلب تنظیمات پروکسی میزبان را دریافت نمی‌کنند و localhost در آن‌ها خود کانتینر است (تنظیمات پروکسی در Linux).
  • یکپارچه‌سازی‌های API با IP خروجی ثابت: رد شدن تونل شبیه از کار افتادن API به نظر می‌رسد (IP ثابت برای API).
  • آزمودن پروکسی تازه: پیش از اجرای کل کار، یک درخواست با مهلت بفرستید (چگونه پروکسی را آزمایش کنیم).
  • خودکارسازی مرورگر: همین شکست تونل به شکل ERR_TUNNEL_CONNECTION_FAILED ظاهر می‌شود (Playwright با پروکسی).
  • شبکه‌های سازمانی: فایروال و پروکسی هر دو اتصال‌ها را پالایش می‌کنند (پروکسی و فایروال).

اشتباهات رایج

  • بالا بردن تعداد تلاش‌های دوباره. علت دائمی دائمی می‌ماند؛ فقط بیشتر منتظر می‌مانید.
  • مقصر دانستن مقصد چون نامش در سطر pool آمده است. در آدرس‌های https:// پروکسی فقط داخل پرانتز دیده می‌شود.
  • فراموش کردن یک متغیر قدیمی HTTPS_PROXY. این متغیر می‌تواند بی‌صدا جای session.proxies را بگیرد.
  • باقی گذاشتن verify=False در محیط عملیاتی. مستندات Requests هشدار می‌دهد که این کار شما را در معرض حمله مرد میانی قرار می‌دهد. certifi را به‌روز کنید یا REQUESTS_CA_BUNDLE را تنظیم کنید؛ برای پروکسی محلی اشکال‌زدایی، پروکسی MITM را ببینید.
  • گرفتن ConnectionError در ابتدا. این کلاس ProxyError، SSLError و ConnectTimeout را هم می‌بلعد.
  • یکی گرفتن 403 و 407 در تونل. یکی قاعده است و دیگری اطلاعات ورود.
  • خواندن سطر آخر pip. علت در سطرهای WARNING بالای آن است.

راهنمای انتخاب

آنچه می‌بینیدچه باید کرد
NameResolutionError در Caused byنامی را که به IP تبدیل نشد اصلاح کنید (میزبان پروکسی یا URL)؛ تلاش دوباره لازم نیست
ProxyError همراه NewConnectionErrorمیزبان و پورت پروکسی را دوباره کپی کنید؛ فایروال محلی را بررسی کنید
Tunnel connection failed: 403پورت مقصد، نوع و پورت پروکسی و قواعد مجاز را بررسی کنید
Tunnel connection failed: 407اطلاعات ورود یا لیست سفید IP را بررسی کنید
SSLError: CERTIFICATE_VERIFY_FAILEDcertifi را به‌روز کنید یا REQUESTS_CA_BUNDLE را تنظیم کنید (در pip: --cert)
فراخوانی‌ها گاهی معلق می‌مانند یا به مهلت می‌رسندtimeout=(3.05, 20) به‌اضافه یک Retry فقط برای اتصال
pip می‌گوید «No matching distribution found»سطر WARNING: Retrying را بخوانید

پرسش‌های متداول

آیا بالا بردن تعداد تلاش‌های دوباره خطای «Max retries exceeded» را برطرف می‌کند؟

فقط وقتی شبکه برای لحظه‌ای قطع شده باشد. برای نامی که به IP تبدیل نمی‌شود، پورت بسته یا تونل ردشده، هر تلاش دوباره به همان شکل شکست می‌خورد. نخست بخش Caused by را بخوانید.

مهلت پیش‌فرض در Python Requests چقدر است؟

مهلت پیش‌فرضی وجود ندارد: بدون timeout= یک درخواست ممکن است بی‌پایان منتظر بماند. به هر فراخوانی یک تاپل (connect, read) بدهید.

مهلت Requests بر حسب ثانیه است یا میلی‌ثانیه؟

ثانیه. عدد اعشاری مانند 3.05 هم کار می‌کند؛ یک عدد تنها هر دو مرحله را تعیین می‌کند و تاپلی مانند (3.05, 20) آن‌ها را جداگانه تعیین می‌کند.

آیا می‌توانم برای CERTIFICATE_VERIFY_FAILED از verify=False استفاده کنم؟

فقط برای یک آزمون سریع محلی، چون این کار به هر کسی در مسیر اجازه می‌دهد ترافیک را بخواند یا تغییر دهد. راه‌حل ماندگار certifi به‌روز است یا، پشت پروکسی سازمانی که TLS را بازرسی می‌کند، قرار دادن گواهی ریشه آن در REQUESTS_CA_BUNDLE.

چرا pip با اینکه بسته وجود دارد می‌گوید «No matching distribution found»؟

pip نتوانست به ایندکس بسته‌ها برسد، پس هیچ نسخه‌ای پیدا نکرد. سطرهای WARNING: Retrying ... after connection broken by در بالای آن علت واقعی را نام می‌برند، مانند Tunnel connection failed: 403 Forbidden.

چرا هنگام فراخوانی ⁦localhost:8000⁩ این خطا را می‌گیرم؟

هیچ برنامه‌ای روی آن پورت گوش نمی‌دهد: سرور خاموش است، از پورت دیگری استفاده می‌کند یا کد شما در Docker اجرا می‌شود که در آن localhost خود کانتینر است. درونی‌ترین خطا [Errno 111] یا [WinError 10061] است. یافتن برنامه‌ای که یک پورت را در اختیار دارد را در پورت 8080 چیست؟ توضیح داده‌ایم.

خلاصه

«Max retries exceeded with url» یک پوشش است: علت پس از Caused by می‌آید و چون Requests به‌طور پیش‌فرض تلاش دوباره نمی‌کند، پس از یک تلاش هم ظاهر می‌شود. در خطاهای پروکسی، شکست در مسیر رسیدن به پروکسی را از تونلی که پروکسی رد کرده جدا کنید و در آدرس پروکسی http:// را نگه دارید. به هر درخواست یک تاپل مهلت بدهید و فقط شکست‌های اتصال را دوباره تلاش کنید. پیکربندی تازه را نخست با یک درخواست بیازمایید (چگونه پروکسی را آزمایش کنیم) و گزینه‌ها را در صفحه خدمات پروکسی ما مقایسه کنید.

پرسش از ChatGPTپرسش از Claude