fix(http): 用urllib3替代requests修复SSL EOF错误

问题根因:
- Python OpenSSL 3.5.4 + requests 2.32.4 + urllib3 2.5.0 版本不兼容
- requests 2.32.4 内部使用 urllib3 的方式与 urllib3 2.5.0 API 不兼容
- curl(SecureTransport)正常工作,但 Python requests(OpenSSL)失败
- 服务器(Caddy)使用 TLS 1.3 + X25519MLKEM768(后量子密钥交换)

修复方案:
- 用 urllib3.PoolManager 直接发起 HTTP 请求(已验证可正常工作)
- 封装 _http_get() 函数替代 requests.get()
- 替换所有 requests 相关异常类型为 urllib3 异常

修改文件:
- datasource/flask_api_source.py: 核心数据源层
- rotation/simple_rotation.py: 简单轮动策略层
This commit is contained in:
2026-06-02 22:22:36 +08:00
parent 74f0eebef0
commit 81045f9d85
2 changed files with 112 additions and 52 deletions

View File

@@ -14,7 +14,8 @@ import sys
import math
import json
import time
import requests
import urllib3
import urllib.parse
import numpy as np
import pandas as pd
from pathlib import Path
@@ -26,6 +27,30 @@ sys.path.insert(0, str(PROJECT_ROOT))
from rotation.config_loader import load_rotation_config, RotationStrategyConfig
# ============================================================
# HTTP client (urllib3 替代 requests修复 SSL EOF 问题)
# ============================================================
_http_pool = urllib3.PoolManager(timeout=urllib3.Timeout(connect=10, read=120))
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)
# ============================================================
# Pure functions: momentum
@@ -122,7 +147,7 @@ class DataCache:
params = {'code': code, 'start': start_date, 'end': end_date, 'adj': adj}
for attempt in range(3):
try:
resp = requests.get(url, params=params, timeout=self.timeout)
resp = _http_get(url, params=params, timeout=self.timeout)
if resp.status_code != 200:
if attempt < 2:
time.sleep(1)
@@ -149,10 +174,12 @@ class DataCache:
self._save_premium_cache(code, df.attrs['premium_series'])
print(f" + {code}: {len(df)} rows ({adj})")
return df
except requests.exceptions.Timeout:
except (urllib3.exceptions.TimeoutError, urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e:
# 网络相关错误超时、SSL、连接断开都进行重试
if attempt < 2:
time.sleep(1 + attempt) # 递增延迟: 1s, 2s
continue
print(f" x {code}: timeout")
print(f" x {code}: {type(e).__name__} after {attempt+1} retries")
return None
except Exception as e:
print(f" x {code}: {e}")
@@ -207,10 +234,10 @@ class DataCache:
params = {'code': code, 'start': start_date, 'end': end_date, 'adj': 'raw'}
for attempt in range(3):
try:
resp = requests.get(url, params=params, timeout=self.timeout)
resp = _http_get(url, params=params, timeout=self.timeout)
if resp.status_code != 200:
if attempt < 2:
time.sleep(1)
time.sleep(1 + attempt)
continue
return
data = resp.json()
@@ -225,8 +252,9 @@ 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 requests.exceptions.Timeout:
except (urllib3.exceptions.TimeoutError, urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError):
if attempt < 2:
time.sleep(1 + attempt)
continue
return
except Exception:
@@ -238,9 +266,10 @@ class DataCache:
params = {'market': market, 'start': start_date, 'end': end_date}
for attempt in range(3):
try:
resp = requests.get(url, params=params, timeout=self.timeout)
resp = _http_get(url, params=params, timeout=self.timeout)
if resp.status_code != 200:
if attempt < 2:
time.sleep(1 + attempt)
continue
return None
data = resp.json()
@@ -253,8 +282,15 @@ 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:
if attempt < 2:
time.sleep(1 + attempt)
continue
print(f" x calendar: {type(e).__name__} after {attempt+1} retries")
return None
except Exception as e:
if attempt < 2:
time.sleep(1 + attempt)
continue
print(f" x calendar: {e}")
return None