fix(http): 用requests+trust_env=False修复SSL EOF问题
根因:Clash代理(127.0.0.1:7890)在处理TLS 1.3+后量子密钥交换时 不兼容,导致SSL EOF错误。requests默认trust_env=True会读取系统 代理配置,通过代理转发HTTPS请求时触发问题。 修复:使用requests.Session(trust_env=False)绕过系统代理, 直连目标服务器。无需降级urllib3版本。 影响文件: - rotation/simple_rotation.py - datasource/flask_api_source.py
This commit is contained in:
@@ -8,8 +8,7 @@ Flask API 数据源
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import urllib3
|
||||
import urllib.parse
|
||||
import requests
|
||||
import pandas as pd
|
||||
from typing import Optional, Dict, List
|
||||
from datetime import datetime
|
||||
@@ -21,23 +20,17 @@ from .models import OHLCVResponse, validate_ohlcv_response
|
||||
load_dotenv()
|
||||
|
||||
# ============================================================
|
||||
# HTTP client (urllib3 替代 requests,修复 SSL EOF 问题)
|
||||
# HTTP client (requests + trust_env=False,绕过系统代理避免 SSL EOF)
|
||||
# ============================================================
|
||||
|
||||
_http_pool = urllib3.PoolManager(
|
||||
maxsize=16, # 支持并行连接
|
||||
timeout=urllib3.Timeout(connect=10, read=120)
|
||||
)
|
||||
# Clash 等代理在处理 TLS 1.3 + 后量子密钥交换时会触发 SSL EOF 错误
|
||||
# trust_env=False 让 requests 忽略环境变量中的代理配置,直连目标服务器
|
||||
_session = requests.Session()
|
||||
_session.trust_env = False
|
||||
|
||||
def _http_get(url: str, params: dict = None, timeout: int = 120) -> urllib3.HTTPResponse:
|
||||
"""使用 urllib3 发起 GET 请求(替代 requests.get,修复 OpenSSL 3.5 + Caddy 的 SSL EOF 问题)"""
|
||||
if params:
|
||||
url = url + '?' + urllib.parse.urlencode(params)
|
||||
return _http_pool.request('GET', url, timeout=urllib3.Timeout(connect=10, read=timeout))
|
||||
|
||||
def _parse_json(resp: urllib3.HTTPResponse) -> dict:
|
||||
"""解析 JSON 响应"""
|
||||
return json.loads(resp.data.decode('utf-8'))
|
||||
def _http_get(url: str, params: dict = None, timeout: int = 120) -> requests.Response:
|
||||
"""使用 requests 发起 GET 请求(trust_env=False 绕过系统代理)"""
|
||||
return _session.get(url, params=params, timeout=timeout)
|
||||
|
||||
|
||||
class FlaskAPIDataSource:
|
||||
@@ -133,15 +126,15 @@ class FlaskAPIDataSource:
|
||||
try:
|
||||
response = _http_get(url, params=params, timeout=self.timeout)
|
||||
|
||||
if response.status != 200:
|
||||
if response.status_code != 200:
|
||||
if attempt < self.retries - 1:
|
||||
time.sleep(1 + attempt)
|
||||
continue
|
||||
print(f"✗ API请求失败: {response.status} - {response.data.decode('utf-8', errors='replace')[:100]}")
|
||||
print(f"✗ API请求失败: {response.status_code} - {response.text[:100]}")
|
||||
return None
|
||||
|
||||
# 解析 JSON
|
||||
data = _parse_json(response)
|
||||
data = response.json()
|
||||
|
||||
# 检查错误
|
||||
if 'error' in data:
|
||||
@@ -210,7 +203,7 @@ class FlaskAPIDataSource:
|
||||
print(f"✓ {code}: {actual_count} 条数据 ({actual_start} ~ {actual_end})")
|
||||
return df
|
||||
|
||||
except urllib3.exceptions.TimeoutError:
|
||||
except requests.exceptions.Timeout:
|
||||
if attempt < self.retries - 1:
|
||||
print(f"⚠ {code}: 请求超时,重试 {attempt + 2}/{self.retries}")
|
||||
time.sleep(1 + attempt)
|
||||
@@ -218,7 +211,7 @@ class FlaskAPIDataSource:
|
||||
print(f"✗ {code}: 请求超时")
|
||||
return None
|
||||
|
||||
except (urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e:
|
||||
except (requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e:
|
||||
if attempt < self.retries - 1:
|
||||
print(f"⚠ {code}: {type(e).__name__},重试 {attempt + 2}/{self.retries}")
|
||||
time.sleep(1 + attempt)
|
||||
@@ -226,7 +219,7 @@ class FlaskAPIDataSource:
|
||||
print(f"✗ {code}: {type(e).__name__} after {self.retries} retries")
|
||||
return None
|
||||
|
||||
except urllib3.exceptions.HTTPError as e:
|
||||
except requests.exceptions.RequestException as e:
|
||||
if attempt < self.retries - 1:
|
||||
time.sleep(1 + attempt)
|
||||
continue
|
||||
@@ -303,10 +296,10 @@ class FlaskAPIDataSource:
|
||||
try:
|
||||
response = _http_get(url, params=params, timeout=self.timeout)
|
||||
|
||||
if response.status != 200:
|
||||
if response.status_code != 200:
|
||||
return None
|
||||
|
||||
data = _parse_json(response)
|
||||
data = response.json()
|
||||
|
||||
if 'error' in data:
|
||||
return None
|
||||
@@ -378,8 +371,8 @@ class FlaskAPIDataSource:
|
||||
|
||||
try:
|
||||
response = _http_get(url, params=params, timeout=self.timeout)
|
||||
if response.status == 200:
|
||||
data = _parse_json(response)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return {
|
||||
'status': 'healthy',
|
||||
'ssh_configured': True,
|
||||
@@ -396,10 +389,10 @@ class FlaskAPIDataSource:
|
||||
|
||||
try:
|
||||
response = _http_get(url, timeout=10)
|
||||
if response.status == 200:
|
||||
return _parse_json(response)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return {"error": f"HTTP {response.status}"}
|
||||
return {"error": f"HTTP {response.status_code}"}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
@@ -442,15 +435,15 @@ class FlaskAPIDataSource:
|
||||
try:
|
||||
response = _http_get(url, params=params, timeout=self.timeout)
|
||||
|
||||
if response.status != 200:
|
||||
if response.status_code != 200:
|
||||
if attempt < self.retries - 1:
|
||||
print(f"⚠ 交易日历请求失败 (HTTP {response.status}),重试 {attempt + 2}/{self.retries}")
|
||||
print(f"⚠ 交易日历请求失败 (HTTP {response.status_code}),重试 {attempt + 2}/{self.retries}")
|
||||
time.sleep(1 + attempt)
|
||||
continue
|
||||
print(f"✗ 交易日历请求失败: HTTP {response.status} - {response.data.decode('utf-8', errors='replace')[:100]}")
|
||||
print(f"✗ 交易日历请求失败: HTTP {response.status_code} - {response.text[:100]}")
|
||||
return None
|
||||
|
||||
data = _parse_json(response)
|
||||
data = response.json()
|
||||
|
||||
# 检查错误
|
||||
if 'error' in data:
|
||||
@@ -471,7 +464,7 @@ class FlaskAPIDataSource:
|
||||
print(f"✓ {market} ({exchange}): {count} 个交易日 ({start_date} ~ {end_date})")
|
||||
return dates
|
||||
|
||||
except urllib3.exceptions.TimeoutError:
|
||||
except requests.exceptions.Timeout:
|
||||
if attempt < self.retries - 1:
|
||||
print(f"⚠ 交易日历请求超时,重试 {attempt + 2}/{self.retries}")
|
||||
time.sleep(1 + attempt)
|
||||
@@ -479,7 +472,7 @@ class FlaskAPIDataSource:
|
||||
print(f"✗ 交易日历请求超时")
|
||||
return None
|
||||
|
||||
except (urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e:
|
||||
except (requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e:
|
||||
if attempt < self.retries - 1:
|
||||
print(f"⚠ 交易日历: {type(e).__name__},重试 {attempt + 2}/{self.retries}")
|
||||
time.sleep(1 + attempt)
|
||||
@@ -487,7 +480,7 @@ class FlaskAPIDataSource:
|
||||
print(f"✗ 交易日历: {type(e).__name__} after {self.retries} retries")
|
||||
return None
|
||||
|
||||
except urllib3.exceptions.HTTPError as e:
|
||||
except requests.exceptions.RequestException as e:
|
||||
if attempt < self.retries - 1:
|
||||
time.sleep(1 + attempt)
|
||||
continue
|
||||
@@ -506,7 +499,7 @@ class FlaskAPIDataSource:
|
||||
|
||||
try:
|
||||
response = _http_get(url, timeout=10)
|
||||
return _parse_json(response)
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ import sys
|
||||
import math
|
||||
import json
|
||||
import time
|
||||
import urllib3
|
||||
import urllib.parse
|
||||
import requests
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
@@ -28,31 +27,17 @@ sys.path.insert(0, str(PROJECT_ROOT))
|
||||
from rotation.config_loader import load_rotation_config, RotationStrategyConfig
|
||||
|
||||
# ============================================================
|
||||
# HTTP client (urllib3 替代 requests,修复 SSL EOF 问题)
|
||||
# HTTP client (requests + trust_env=False,绕过系统代理避免 SSL EOF)
|
||||
# ============================================================
|
||||
|
||||
_http_pool = urllib3.PoolManager(
|
||||
maxsize=16, # 支持并行连接
|
||||
timeout=urllib3.Timeout(connect=10, read=120)
|
||||
)
|
||||
# Clash 等代理在处理 TLS 1.3 + 后量子密钥交换时会触发 SSL EOF 错误
|
||||
# trust_env=False 让 requests 忽略环境变量中的代理配置,直连目标服务器
|
||||
_session = requests.Session()
|
||||
_session.trust_env = False
|
||||
|
||||
class _HttpResponse:
|
||||
"""urllib3 响应包装,提供 requests 兼容接口"""
|
||||
def __init__(self, resp):
|
||||
self.status_code = resp.status
|
||||
self._data = resp.data
|
||||
self._json = None
|
||||
def json(self):
|
||||
if self._json is None:
|
||||
self._json = json.loads(self._data)
|
||||
return self._json
|
||||
|
||||
def _http_get(url: str, params: dict = None, timeout: int = 120) -> _HttpResponse:
|
||||
"""使用 urllib3 发起 GET 请求(替代 requests.get,修复 OpenSSL 3.5 + Caddy 的 SSL EOF 问题)"""
|
||||
if params:
|
||||
url = url + '?' + urllib.parse.urlencode(params)
|
||||
resp = _http_pool.request('GET', url, timeout=urllib3.Timeout(connect=10, read=timeout))
|
||||
return _HttpResponse(resp)
|
||||
def _http_get(url: str, params: dict = None, timeout: int = 120) -> requests.Response:
|
||||
"""使用 requests 发起 GET 请求(trust_env=False 绕过系统代理)"""
|
||||
return _session.get(url, params=params, timeout=timeout)
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -177,7 +162,7 @@ class DataCache:
|
||||
self._save_premium_cache(code, df.attrs['premium_series'])
|
||||
print(f" + {code}: {len(df)} rows ({adj})")
|
||||
return df
|
||||
except (urllib3.exceptions.TimeoutError, urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e:
|
||||
except (requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e:
|
||||
# 网络相关错误(超时、SSL、连接断开)都进行重试
|
||||
if attempt < 2:
|
||||
time.sleep(1 + attempt) # 递增延迟: 1s, 2s
|
||||
@@ -255,7 +240,7 @@ class DataCache:
|
||||
self._save_premium_cache(code, self.premium_data[code])
|
||||
print(f" + premium {code}: +{len(new_data)} days (total {len(self.premium_data[code])})")
|
||||
return
|
||||
except (urllib3.exceptions.TimeoutError, urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError):
|
||||
except (requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ConnectionError):
|
||||
if attempt < 2:
|
||||
time.sleep(1 + attempt)
|
||||
continue
|
||||
@@ -285,7 +270,7 @@ class DataCache:
|
||||
result = pd.DatetimeIndex(dates)
|
||||
print(f" + {market}: {len(result)} trading days ({start_date} ~ {end_date})")
|
||||
return result
|
||||
except (urllib3.exceptions.TimeoutError, urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e:
|
||||
except (requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e:
|
||||
if attempt < 2:
|
||||
time.sleep(1 + attempt)
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user