# Requests에서 ProxyError, SSLError 및 ConnectTimeout 해결하기

requests.exceptions.ProxyError는 증상일 뿐, 원인이 아닙니다. 여기서는 예외 계층 구조를 해독하고, 각 traceback을 실제 해결책과 매칭하며, 실패를 분류하고 죽은 출구를 회전하는 건강 검사 기능을 제공합니다.

`requests.exceptions.ProxyError`는 Python에서 가장 도움이 되지 않는 오류 메시지 중 하나입니다: 죽은 프록시, 잘못된 자격 증명, 잘못된 스킴, 과부하된 출구 및 방화벽 차단에 대해 거의 동일한 traceback을 발생시킵니다. 빠르게 해결하는 요령은 ProxyError가 근본 원인이 아니라는 것을 아는 것입니다 — 이는 범주입니다. Requests 소스에서 `ProxyError`, `SSLError` 및 `ConnectTimeout`은 모두 `ConnectionError`의 하위 클래스이며, 각각은 요청 수명 주기의 특정 단계에서 발생합니다. 단계를 읽으면 원인을 읽을 수 있습니다. 이 가이드는 예외 계층 구조를 해독하고, 일반적인 traceback을 실제 해결책과 매칭하며, 실패를 분류하고 자동으로 죽어가는 출구를 회전하는 건강 검사 기능을 제공합니다.

## Requests 예외 계층 구조

Requests의 모든 프록시 관련 오류는 `RequestException`에서 파생됩니다. 디버깅에 유용한 분기는 `ConnectionError`입니다. 실제로 발생하는 세 가지 예외가 모두 그 아래에 있기 때문입니다:

- **ProxyError** — 프록시에 자체적으로 연결이 실패할 때 발생: 도달할 수 없는 호스트, 잘못된 포트, 거부된 연결 또는 거부된 `407`. 메시지는 종종 `HTTPSConnectionPool(...)` 래퍼 내부에 `Cannot connect to proxy`를 중첩합니다.
- **SSLError** — 프록시에 연결되었지만 대상과의 TLS 핸드셰이크가 실패했습니다. 프록시 설정에서 일반적인 트리거는 `https` 키 안에 `https://`가 아닌 `http://`로 작성된 것입니다.
- **ConnectTimeout** — 프록시가 연결 창 내에 응답하지 않았습니다. 이는 `ConnectionError`와 `Timeout` 모두의 하위 클래스이며, 명시적으로 재시도해도 안전하다고 문서화되어 있습니다.
- **ReadTimeout** — 프록시가 연결되어 요청을 전달했지만 대상이 응답하기에 너무 느렸습니다. 이는 구성 버그가 아닌 대상 또는 출구 품질 문제입니다.
- **MissingSchema / InvalidProxyURL** — 프록시 URL이 잘못된 경우 네트워크 호출 전에 발생합니다. 이는 순수한 오타입니다.

첫 세 가지가 부모를 공유하기 때문에, 하나의 `except requests.exceptions.ConnectionError`가 모든 것을 재시도 로직으로 포착합니다 — 하위 클래스를 개별적으로 포착하면 각 실패 이유를 기록할 수 있습니다. 이러한 대부분을 피하는 깨끗한 설정은 우리의 [Python Requests 프록시 가이드](https://quantumproxies.io/blog/python-requests-proxy-guide)에 다루어져 있습니다; 이 게시물은 traceback이 이미 화면에 나타났을 때 해야 할 일에 관한 것입니다.

어떤 습관이 단일 해결책보다 더 많은 시간을 절약합니다: traceback을 아래에서 위로 읽으세요. Requests는 기본 urllib3 실패를 래핑하므로, 상위 프레임은 호출이 *어디서* 이루어졌는지를 설명하고 하위 프레임은 *무엇이* 잘못되었는지를 설명합니다. 원하는 줄은 가장 안쪽의 `Caused by` 절입니다 — 이는 외부 `ProxyError` 또는 `ConnectionError`가 단순히 다시 발생시키는 구체적인 실패(거부된 연결, 인증서 불일치, 구문 분석 오류)를 명명합니다. 그 줄을 읽을 수 있게 되면, 이 가이드의 나머지는 조회표입니다.

## ProxyError 이전의 고전적인 ValueError

가장 많이 검색된 프록시 traceback은 ProxyError가 아닙니다 — 그것은 `ValueError: invalid literal for int() with base 10`이며, urllib3 깊숙이에서 발생합니다. 이는 스킴 없이 프록시 값에 자격 증명을 포함할 때 발생하며, 파서가 콜론 뒤의 텍스트를 포트 번호로 읽습니다:

```python
# 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}@residentialpremium.quantumproxies.io:8080",
    "https": f"http://user:{pw}@residentialpremium.quantumproxies.io:8080",
}
```

![네 단계로 본 요청 수명 주기와 각 단계에서 발생하는 예외: 프록시 URL 구성 단계는 MissingSchema, 프록시 연결 단계는 ProxyError 또는 ConnectTimeout, TLS 핸드셰이크는 SSLError, 대상 응답 대기는 ReadTimeout을 발생시킵니다. 카드는 어떤 것은 고치고 어떤 것은 재시도해야 하는지 알려줍니다.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335156/blog/generated/qp-b9-018-diag1.png)
*수명 주기 단계가 원인을 명명합니다: 빌드 타임 오타는 MissingSchema, 실패한 프록시 연결은 ProxyError, TLS 실패는 SSLError, 느린 출구는 ReadTimeout입니다.*

## 실패를 분류하는 프록시 건강 검사

추측 대신 각 예외 유형을 포착하고 이를 평범한 영어 판결로 변환하세요. 이 함수는 성공 시 출구 IP를 반환하고 실패 시 라벨이 붙은 이유를 반환합니다 — 스크랩을 시작하기 전에 프록시가 살아 있는지 확인하기 위해 이를 사용하세요. 이는 [주거용 프록시](https://quantumproxies.io/residential-proxies)를 포함한 모든 인증 게이트웨이에 대해 작동합니다:

```python
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)
```

## curl은 작동하지만 Python은 ProxyError를 발생시키는 경우

같은 자격 증명이 curl에서는 성공하지만 Python에서는 ProxyError를 발생시키는 경우, 환경 변수가 거의 항상 당신의 딕셔너리를 재정의하고 있습니다. Requests는 셸에서 `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`를 읽으며, 오래된 기업 값이 모든 호출을 조용히 재라우팅합니다. 실제로 사용 중인 것을 보려면 `session.proxies`를 출력한 다음 환경 조회를 완전히 비활성화하세요:

```python
import requests

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

올바른 자격 증명이 있는 `407`이 지속된다면, 등록되지 않은 주소에서 호출된 IP 화이트리스트 계획을 가리킵니다 — 원인 전체 목록은 우리의 [407 Proxy Authentication Required 가이드](https://quantumproxies.io/blog/fix-407-proxy-authentication-required)에 있습니다.

## 간헐적인 ProxyError: 재시작하지 말고 회전하세요

짜증나는 경우는 코드가 20분 동안 실행된 후 ProxyError를 발생시키고, 다시 작동하는 경우입니다. 이는 스크립트의 버그가 아닙니다 — 이는 실행 중간에 죽거나 속도 제한된 단일 출구 IP입니다. 해결책은 회전으로 재시도하는 것입니다: 호출을 래핑하고, `ConnectionError`를 포착하며, 회전 게이트웨이가 다음 시도에서 새로운 IP를 제공하도록 하세요. [회전 프록시](https://quantumproxies.io/rotating-proxies)를 통해 모든 재시도는 다른 출구를 통해 이동하므로, 하나의 죽은 주소가 요청을 두 번 실패시킬 수 없습니다:

```python
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}")
```

ProxyError가 여러 새로운 IP에서 지속된다면, 문제는 풀에서 대상로 이동한 것입니다: 차단되고 있는 것입니다, 연결이 끊어진 것이 아닙니다. 이는 다른 싸움입니다 — 페이싱, 헤더 및 세션 위생에 대한 [차단 방지 체크리스트](https://quantumproxies.io/blog/avoid-ip-ban-web-scraping)를 참조하세요.

내부화할 가치가 있는 또 다른 구별이 있습니다, 왜냐하면 그것이 당신의 대응 방식을 바꾸기 때문입니다. `ProxyError` 또는 `ConnectTimeout`은 요청이 완료되지 않았음을 의미하므로, 재시도가 안전합니다 — POST에도 안전합니다 — 반대편에서 아무 일도 일어나지 않았습니다. `ReadTimeout`은 반대로 대상이 요청을 받았고 단순히 응답하는 데 너무 오래 걸렸음을 의미합니다; 비멱등적 쓰기에 대한 재시도는 이중 제출을 초래할 수 있습니다. 재시도 루프를 구축할 때, 연결 단계 실패를 자유롭게 재시도할 수 있는 것으로 취급하고, 읽기 단계 실패는 GET 및 HEAD에 대해서만 재시도할 수 있는 것으로 취급하세요. 이 단일 규칙은 불안정한 프록시가 하나의 체크아웃을 세 개로 변환하는 미묘한 버그를 방지합니다.

![자격 증명은 있지만 스킴이 없는 proxies 딕셔너리는 ValueError: invalid literal for int() with base 10을 발생시키고, 수정된 버전은 http://와 URL 인코딩된 비밀번호를 추가합니다. 카드는 스킴 누락부터 간헐적 실패 시 출구 회전까지, 흔한 오류 메시지와 한 줄 해결책을 짝지어 보여줍니다.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335157/blog/generated/qp-b9-018-diag2.png)
*메시지를 읽고, 해결책을 적용하세요: 잘못된 호스트, 누락된 스킴, 잘못된 TLS 키, 인코딩되지 않은 비밀번호 또는 죽어가는 출구는 각각 단일 수정 단계에 매핑됩니다.*

## 자주 묻는 질문

### requests.exceptions.ProxyError: 프록시에 연결할 수 없음의 원인은 무엇입니까?

프록시 호스트 또는 포트가 잘못되었거나, 프록시가 다운되었거나, 방화벽이 요청이 떠나기 전에 연결을 차단하고 있습니다. 동일한 자격 증명을 사용하여 `curl -x`로 엔드포인트를 확인하세요; curl도 실패하면 프록시에 도달할 수 없으며, curl이 성공하면 환경 변수나 Python의 잘못된 딕셔너리가 원인입니다.

### 왜 ProxyError가 HTTPSConnectionPool에 래핑되어 있습니까?

그 래퍼는 단지 urllib3가 대상을 도달하기 위해 사용한 연결 풀의 이름을 지정합니다 — 이는 내부에 중첩된 실제 메시지 주위의 잡음입니다. 가장 안쪽의 `Caused by` 절을 읽으세요: `Cannot connect to proxy`는 연결 실패를 의미하며, 동일한 풀 내의 `407` 또는 SSL 메시지는 인증 또는 TLS/스킴 문제를 가리킵니다.

### Requests가 환경에서 프록시를 읽지 않도록 하려면 어떻게 해야 합니까?

`session.trust_env = False`를 세션에 설정하거나, `trust_env=False`를 전달하여 Requests가 `HTTP_PROXY` 및 `HTTPS_PROXY`를 무시하도록 하세요. 이는 한 셸에서는 프록시가 작동하지만 다른 셸에서는 ProxyError를 발생시키거나, 기업 변수가 프록시를 사용하도록 구성하지 않은 스크래퍼를 납치할 때의 해결책입니다.

### ConnectTimeout은 안전하게 재시도할 수 있습니까?

예 — Requests 문서는 `ConnectTimeout`이 서버에 도달하지 않았기 때문에 안전하게 재시도할 수 있다고 표시합니다, 따라서 부작용이 발생할 수 없습니다. 이를 재시도하세요, 이상적으로는 회전 게이트웨이를 통해 다음 시도가 다른, 더 빠른 출구를 사용하도록 하세요. `ReadTimeout`은 POST와 같은 비멱등적 메서드에 대해 맹목적으로 재시도하기에는 더 위험합니다.

ProxyError를 단일 오류로 취급하는 것을 멈추고 수명 주기 단계별로 읽기 시작하면, 해결책은 기계적입니다: 스킴 오타는 네트워크 전에 발생하고, 연결 실패는 죽은 프록시를 명명하며, SSL 오류는 잘못된 키를 명명하고, 간헐적인 실패는 재시작이 아닌 회전을 원합니다. 깨끗한 출구는 대부분을 완전히 제거합니다.

[살아있는 주거용 IP로 스크랩하기](https://quantumproxies.io/residential-proxies)
