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

@@ -7,7 +7,9 @@ Flask API 数据源
import os
import json
import requests
import time
import urllib3
import urllib.parse
import pandas as pd
from typing import Optional, Dict, List
from datetime import datetime
@@ -18,6 +20,22 @@ from .models import OHLCVResponse, validate_ohlcv_response
load_dotenv()
# ============================================================
# HTTP client (urllib3 替代 requests修复 SSL EOF 问题)
# ============================================================
_http_pool = urllib3.PoolManager()
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'))
class FlaskAPIDataSource:
"""
@@ -110,24 +128,17 @@ class FlaskAPIDataSource:
for attempt in range(self.retries):
try:
response = requests.get(
url,
params=params,
timeout=self.timeout
)
response = _http_get(url, params=params, timeout=self.timeout)
if response.status_code != 200:
if response.status != 200:
if attempt < self.retries - 1:
time.sleep(1 + attempt)
continue
print(f"✗ API请求失败: {response.status_code} - {response.text[:100]}")
print(f"✗ API请求失败: {response.status} - {response.data.decode('utf-8', errors='replace')[:100]}")
return None
# 尝试解析 JSON(支持 zstd 响应)
try:
data = response.json()
except (json.JSONDecodeError, requests.exceptions.JSONDecodeError):
# 如果 response.json() 失败,手动解析
data = json.loads(response.text)
# 解析 JSON
data = _parse_json(response)
# 检查错误
if 'error' in data:
@@ -196,15 +207,25 @@ class FlaskAPIDataSource:
print(f"{code}: {actual_count} 条数据 ({actual_start} ~ {actual_end})")
return df
except requests.exceptions.Timeout:
except urllib3.exceptions.TimeoutError:
if attempt < self.retries - 1:
print(f"{code}: 请求超时,重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt)
continue
print(f"{code}: 请求超时")
return None
except requests.exceptions.RequestException as e:
except (urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e:
if attempt < self.retries - 1:
print(f"{code}: {type(e).__name__},重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt)
continue
print(f"{code}: {type(e).__name__} after {self.retries} retries")
return None
except urllib3.exceptions.HTTPError as e:
if attempt < self.retries - 1:
time.sleep(1 + attempt)
continue
print(f"{code}: 请求异常 - {e}")
return None
@@ -277,16 +298,12 @@ class FlaskAPIDataSource:
}
try:
response = requests.get(url, params=params, timeout=self.timeout)
response = _http_get(url, params=params, timeout=self.timeout)
if response.status_code != 200:
if response.status != 200:
return None
# 处理 zstd 响应
try:
data = response.json()
except (json.JSONDecodeError, requests.exceptions.JSONDecodeError):
data = json.loads(response.text)
data = _parse_json(response)
if 'error' in data:
return None
@@ -357,9 +374,9 @@ class FlaskAPIDataSource:
params = {'code': '000300.SH', 'start': '2024-01-01', 'end': '2024-01-05'}
try:
response = requests.get(url, params=params, timeout=self.timeout)
if response.status_code == 200:
data = response.json()
response = _http_get(url, params=params, timeout=self.timeout)
if response.status == 200:
data = _parse_json(response)
return {
'status': 'healthy',
'ssh_configured': True,
@@ -375,11 +392,11 @@ class FlaskAPIDataSource:
url = f"{self.base_url}/api/v1/calendar/info"
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
return response.json()
response = _http_get(url, timeout=10)
if response.status == 200:
return _parse_json(response)
else:
return {"error": f"HTTP {response.status_code}"}
return {"error": f"HTTP {response.status}"}
except Exception as e:
return {"error": str(e)}
@@ -420,20 +437,17 @@ class FlaskAPIDataSource:
for attempt in range(self.retries):
try:
response = requests.get(
url,
params=params,
timeout=self.timeout
)
response = _http_get(url, params=params, timeout=self.timeout)
if response.status_code != 200:
if response.status != 200:
if attempt < self.retries - 1:
print(f"⚠ 交易日历请求失败 (HTTP {response.status_code}),重试 {attempt + 2}/{self.retries}")
print(f"⚠ 交易日历请求失败 (HTTP {response.status}),重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt)
continue
print(f"✗ 交易日历请求失败: HTTP {response.status_code} - {response.text[:100]}")
print(f"✗ 交易日历请求失败: HTTP {response.status} - {response.data.decode('utf-8', errors='replace')[:100]}")
return None
data = response.json()
data = _parse_json(response)
# 检查错误
if 'error' in data:
@@ -454,20 +468,30 @@ class FlaskAPIDataSource:
print(f"{market} ({exchange}): {count} 个交易日 ({start_date} ~ {end_date})")
return dates
except requests.exceptions.Timeout:
except urllib3.exceptions.TimeoutError:
if attempt < self.retries - 1:
print(f"⚠ 交易日历请求超时,重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt)
continue
print(f"✗ 交易日历请求超时")
return None
except requests.exceptions.RequestException as e:
except (urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e:
if attempt < self.retries - 1:
print(f"⚠ 交易日历: {type(e).__name__},重试 {attempt + 2}/{self.retries}")
time.sleep(1 + attempt)
continue
print(f"✗ 交易日历: {type(e).__name__} after {self.retries} retries")
return None
except urllib3.exceptions.HTTPError as e:
if attempt < self.retries - 1:
time.sleep(1 + attempt)
continue
print(f"✗ 交易日历请求异常: {e}")
return None
except (json.JSONDecodeError, requests.exceptions.JSONDecodeError) as e:
except json.JSONDecodeError as e:
print(f"✗ 交易日历 JSON 解析失败: {e}")
return None
@@ -478,8 +502,8 @@ class FlaskAPIDataSource:
url = f"{self.base_url}/"
try:
response = requests.get(url, timeout=10)
return response.json()
response = _http_get(url, timeout=10)
return _parse_json(response)
except Exception as e:
return {"error": str(e)}