From 282aa4182c59322e277f1dcdafdb84953775d253 Mon Sep 17 00:00:00 2001 From: AIalliAI <285906080+AIalliAI@users.noreply.github.com> Date: Tue, 7 Jul 2026 06:50:40 +0000 Subject: [PATCH 1/2] fix(weixin): mitigate aiohttp connection pool poisoning (#60025) Add enable_cleanup_closed=True to the TCPConnector used for iLink connections so that server-closed keep-alive connections are properly cleaned up from the pool. Also adds a single retry when an asyncio.TimeoutError or aiohttp.ClientError is caught. Co-authored-by: Hermes Agent Ref aio-libs/aiohttp#12795 --- gateway/platforms/weixin.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/gateway/platforms/weixin.py b/gateway/platforms/weixin.py index d78eb4aad6dc..b0d7ca8fafd1 100644 --- a/gateway/platforms/weixin.py +++ b/gateway/platforms/weixin.py @@ -123,6 +123,11 @@ def _make_ssl_connector() -> Optional["aiohttp.TCPConnector"]: When ``certifi`` is installed, use its Mozilla CA bundle to guarantee verification. Otherwise fall back to aiohttp's default (which honors ``SSL_CERT_FILE`` env var via ``trust_env=True``). + + ``enable_cleanup_closed=True`` prevents connection pool poisoning + (aiohttp#12795) when the iLink server closes idle keep-alive connections + after ~2-3 minutes. Without this, the stale connection remains in the pool, + gets handed out to the next request, and causes an immediate read timeout. """ try: import ssl @@ -132,7 +137,7 @@ def _make_ssl_connector() -> Optional["aiohttp.TCPConnector"]: if not AIOHTTP_AVAILABLE: return None ssl_ctx = ssl.create_default_context(cafile=certifi.where()) - return aiohttp.TCPConnector(ssl=ssl_ctx) + return aiohttp.TCPConnector(ssl=ssl_ctx, enable_cleanup_closed=True) ITEM_TEXT = 1 ITEM_IMAGE = 2 @@ -387,7 +392,13 @@ async def _do() -> Dict[str, Any]: if not response.ok: raise RuntimeError(f"iLink POST {endpoint} HTTP {response.status}: {raw[:200]}") return json.loads(raw) - return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) + try: + return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) + except (asyncio.TimeoutError, aiohttp.ClientError): + # Stale pooled connection (aiohttp#12795): the TCPConnector may hand + # out a server-closed keep-alive connection. Retry once — the second + # attempt creates a fresh TCP connection. + return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) async def _api_get( @@ -411,7 +422,10 @@ async def _do() -> Dict[str, Any]: if not response.ok: raise RuntimeError(f"iLink GET {endpoint} HTTP {response.status}: {raw[:200]}") return json.loads(raw) - return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) + try: + return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) + except (asyncio.TimeoutError, aiohttp.ClientError): + return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) async def _get_updates( From 311d5741372e45a977289ce70b327090a72d6ee8 Mon Sep 17 00:00:00 2001 From: AIalliAI <285906080+AIalliAI@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:08:56 +0000 Subject: [PATCH 2/2] fix(weixin): guard aiohttp.ClientError against None when import fails When aiohttp is not available, aiohttp is set to None. Referencing aiohttp.ClientError in except clauses caused AttributeError. Define _ClientError fallback to Exception when AIOHTTP_AVAILABLE is False. --- gateway/platforms/weixin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gateway/platforms/weixin.py b/gateway/platforms/weixin.py index b0d7ca8fafd1..5de8ff287bbf 100644 --- a/gateway/platforms/weixin.py +++ b/gateway/platforms/weixin.py @@ -43,6 +43,12 @@ aiohttp = None # type: ignore[assignment] AIOHTTP_AVAILABLE = False +# Fallback exception class when aiohttp is not available +if AIOHTTP_AVAILABLE: + _ClientError = aiohttp.ClientError +else: + _ClientError = Exception + try: from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -394,7 +400,7 @@ async def _do() -> Dict[str, Any]: return json.loads(raw) try: return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (asyncio.TimeoutError, _ClientError): # Stale pooled connection (aiohttp#12795): the TCPConnector may hand # out a server-closed keep-alive connection. Retry once — the second # attempt creates a fresh TCP connection. @@ -424,7 +430,7 @@ async def _do() -> Dict[str, Any]: return json.loads(raw) try: return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000) - except (asyncio.TimeoutError, aiohttp.ClientError): + except (asyncio.TimeoutError, _ClientError): return await asyncio.wait_for(_do(), timeout=timeout_ms / 1000)