From 81045f9d85c6e033a45f201dc32fcfb084e79836 Mon Sep 17 00:00:00 2001 From: aszerW Date: Tue, 2 Jun 2026 22:22:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(http):=20=E7=94=A8urllib3=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?requests=E4=BF=AE=E5=A4=8DSSL=20EOF=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题根因: - 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: 简单轮动策略层 --- datasource/flask_api_source.py | 112 ++++++++++++++++++++------------- rotation/simple_rotation.py | 52 ++++++++++++--- 2 files changed, 112 insertions(+), 52 deletions(-) diff --git a/datasource/flask_api_source.py b/datasource/flask_api_source.py index 393666f..306587f 100644 --- a/datasource/flask_api_source.py +++ b/datasource/flask_api_source.py @@ -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)} diff --git a/rotation/simple_rotation.py b/rotation/simple_rotation.py index 56ef46e..81a3125 100644 --- a/rotation/simple_rotation.py +++ b/rotation/simple_rotation.py @@ -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