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:
2026-06-03 00:35:49 +08:00
parent a2b4289080
commit d1139a9ee9
2 changed files with 42 additions and 64 deletions

View File

@@ -8,8 +8,7 @@ Flask API 数据源
import os import os
import json import json
import time import time
import urllib3 import requests
import urllib.parse
import pandas as pd import pandas as pd
from typing import Optional, Dict, List from typing import Optional, Dict, List
from datetime import datetime from datetime import datetime
@@ -21,23 +20,17 @@ from .models import OHLCVResponse, validate_ohlcv_response
load_dotenv() load_dotenv()
# ============================================================ # ============================================================
# HTTP client (urllib3 替代 requests修复 SSL EOF 问题) # HTTP client (requests + trust_env=False绕过系统代理避免 SSL EOF)
# ============================================================ # ============================================================
_http_pool = urllib3.PoolManager( # Clash 等代理在处理 TLS 1.3 + 后量子密钥交换时会触发 SSL EOF 错误
maxsize=16, # 支持并行连接 # trust_env=False 让 requests 忽略环境变量中的代理配置,直连目标服务器
timeout=urllib3.Timeout(connect=10, read=120) _session = requests.Session()
) _session.trust_env = False
def _http_get(url: str, params: dict = None, timeout: int = 120) -> urllib3.HTTPResponse: def _http_get(url: str, params: dict = None, timeout: int = 120) -> requests.Response:
"""使用 urllib3 发起 GET 请求(替代 requests.get修复 OpenSSL 3.5 + Caddy 的 SSL EOF 问题""" """使用 requests 发起 GET 请求(trust_env=False 绕过系统代理"""
if params: return _session.get(url, params=params, timeout=timeout)
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'))
class FlaskAPIDataSource: class FlaskAPIDataSource:
@@ -133,15 +126,15 @@ class FlaskAPIDataSource:
try: try:
response = _http_get(url, params=params, timeout=self.timeout) response = _http_get(url, params=params, timeout=self.timeout)
if response.status != 200: if response.status_code != 200:
if attempt < self.retries - 1: if attempt < self.retries - 1:
time.sleep(1 + attempt) time.sleep(1 + attempt)
continue 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 return None
# 解析 JSON # 解析 JSON
data = _parse_json(response) data = response.json()
# 检查错误 # 检查错误
if 'error' in data: if 'error' in data:
@@ -210,7 +203,7 @@ class FlaskAPIDataSource:
print(f"{code}: {actual_count} 条数据 ({actual_start} ~ {actual_end})") print(f"{code}: {actual_count} 条数据 ({actual_start} ~ {actual_end})")
return df return df
except urllib3.exceptions.TimeoutError: except requests.exceptions.Timeout:
if attempt < self.retries - 1: if attempt < self.retries - 1:
print(f"{code}: 请求超时,重试 {attempt + 2}/{self.retries}") print(f"{code}: 请求超时,重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt) time.sleep(1 + attempt)
@@ -218,7 +211,7 @@ class FlaskAPIDataSource:
print(f"{code}: 请求超时") print(f"{code}: 请求超时")
return None 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: if attempt < self.retries - 1:
print(f"{code}: {type(e).__name__},重试 {attempt + 2}/{self.retries}") print(f"{code}: {type(e).__name__},重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt) time.sleep(1 + attempt)
@@ -226,7 +219,7 @@ class FlaskAPIDataSource:
print(f"{code}: {type(e).__name__} after {self.retries} retries") print(f"{code}: {type(e).__name__} after {self.retries} retries")
return None return None
except urllib3.exceptions.HTTPError as e: except requests.exceptions.RequestException as e:
if attempt < self.retries - 1: if attempt < self.retries - 1:
time.sleep(1 + attempt) time.sleep(1 + attempt)
continue continue
@@ -303,10 +296,10 @@ class FlaskAPIDataSource:
try: try:
response = _http_get(url, params=params, timeout=self.timeout) response = _http_get(url, params=params, timeout=self.timeout)
if response.status != 200: if response.status_code != 200:
return None return None
data = _parse_json(response) data = response.json()
if 'error' in data: if 'error' in data:
return None return None
@@ -378,8 +371,8 @@ class FlaskAPIDataSource:
try: try:
response = _http_get(url, params=params, timeout=self.timeout) response = _http_get(url, params=params, timeout=self.timeout)
if response.status == 200: if response.status_code == 200:
data = _parse_json(response) data = response.json()
return { return {
'status': 'healthy', 'status': 'healthy',
'ssh_configured': True, 'ssh_configured': True,
@@ -396,10 +389,10 @@ class FlaskAPIDataSource:
try: try:
response = _http_get(url, timeout=10) response = _http_get(url, timeout=10)
if response.status == 200: if response.status_code == 200:
return _parse_json(response) return response.json()
else: else:
return {"error": f"HTTP {response.status}"} return {"error": f"HTTP {response.status_code}"}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}
@@ -442,15 +435,15 @@ class FlaskAPIDataSource:
try: try:
response = _http_get(url, params=params, timeout=self.timeout) response = _http_get(url, params=params, timeout=self.timeout)
if response.status != 200: if response.status_code != 200:
if attempt < self.retries - 1: 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) time.sleep(1 + attempt)
continue 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 return None
data = _parse_json(response) data = response.json()
# 检查错误 # 检查错误
if 'error' in data: if 'error' in data:
@@ -471,7 +464,7 @@ class FlaskAPIDataSource:
print(f"{market} ({exchange}): {count} 个交易日 ({start_date} ~ {end_date})") print(f"{market} ({exchange}): {count} 个交易日 ({start_date} ~ {end_date})")
return dates return dates
except urllib3.exceptions.TimeoutError: except requests.exceptions.Timeout:
if attempt < self.retries - 1: if attempt < self.retries - 1:
print(f"⚠ 交易日历请求超时,重试 {attempt + 2}/{self.retries}") print(f"⚠ 交易日历请求超时,重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt) time.sleep(1 + attempt)
@@ -479,7 +472,7 @@ class FlaskAPIDataSource:
print(f"✗ 交易日历请求超时") print(f"✗ 交易日历请求超时")
return None 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: if attempt < self.retries - 1:
print(f"⚠ 交易日历: {type(e).__name__},重试 {attempt + 2}/{self.retries}") print(f"⚠ 交易日历: {type(e).__name__},重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt) time.sleep(1 + attempt)
@@ -487,7 +480,7 @@ class FlaskAPIDataSource:
print(f"✗ 交易日历: {type(e).__name__} after {self.retries} retries") print(f"✗ 交易日历: {type(e).__name__} after {self.retries} retries")
return None return None
except urllib3.exceptions.HTTPError as e: except requests.exceptions.RequestException as e:
if attempt < self.retries - 1: if attempt < self.retries - 1:
time.sleep(1 + attempt) time.sleep(1 + attempt)
continue continue
@@ -506,7 +499,7 @@ class FlaskAPIDataSource:
try: try:
response = _http_get(url, timeout=10) response = _http_get(url, timeout=10)
return _parse_json(response) return response.json()
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}

View File

@@ -14,8 +14,7 @@ import sys
import math import math
import json import json
import time import time
import urllib3 import requests
import urllib.parse
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from pathlib import Path from pathlib import Path
@@ -28,31 +27,17 @@ sys.path.insert(0, str(PROJECT_ROOT))
from rotation.config_loader import load_rotation_config, RotationStrategyConfig 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( # Clash 等代理在处理 TLS 1.3 + 后量子密钥交换时会触发 SSL EOF 错误
maxsize=16, # 支持并行连接 # trust_env=False 让 requests 忽略环境变量中的代理配置,直连目标服务器
timeout=urllib3.Timeout(connect=10, read=120) _session = requests.Session()
) _session.trust_env = False
class _HttpResponse: def _http_get(url: str, params: dict = None, timeout: int = 120) -> requests.Response:
"""urllib3 响应包装,提供 requests 兼容接口""" """使用 requests 发起 GET 请求trust_env=False 绕过系统代理)"""
def __init__(self, resp): return _session.get(url, params=params, timeout=timeout)
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)
# ============================================================ # ============================================================
@@ -177,7 +162,7 @@ class DataCache:
self._save_premium_cache(code, df.attrs['premium_series']) self._save_premium_cache(code, df.attrs['premium_series'])
print(f" + {code}: {len(df)} rows ({adj})") print(f" + {code}: {len(df)} rows ({adj})")
return df 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、连接断开都进行重试 # 网络相关错误超时、SSL、连接断开都进行重试
if attempt < 2: if attempt < 2:
time.sleep(1 + attempt) # 递增延迟: 1s, 2s time.sleep(1 + attempt) # 递增延迟: 1s, 2s
@@ -255,7 +240,7 @@ class DataCache:
self._save_premium_cache(code, self.premium_data[code]) self._save_premium_cache(code, self.premium_data[code])
print(f" + premium {code}: +{len(new_data)} days (total {len(self.premium_data[code])})") print(f" + premium {code}: +{len(new_data)} days (total {len(self.premium_data[code])})")
return 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: if attempt < 2:
time.sleep(1 + attempt) time.sleep(1 + attempt)
continue continue
@@ -285,7 +270,7 @@ class DataCache:
result = pd.DatetimeIndex(dates) result = pd.DatetimeIndex(dates)
print(f" + {market}: {len(result)} trading days ({start_date} ~ {end_date})") print(f" + {market}: {len(result)} trading days ({start_date} ~ {end_date})")
return result 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: if attempt < 2:
time.sleep(1 + attempt) time.sleep(1 + attempt)
continue continue