fix(weixin): resolve 'Future attached to a different loop' in aiohttp 3.13+ - #20956
fix(weixin): resolve 'Future attached to a different loop' in aiohttp 3.13+#20956rj-chenlinfeng wants to merge 4 commits into
Conversation
aiohttp ClientTimeout uses BaseTimerContext which calls loop.call_later() internally. When invoked via asyncio.run_coroutine_threadsafe() from cron jobs, this triggers "Timeout context manager should be used inside a task" errors, causing message delivery failures. Replace all direct ClientTimeout usage with asyncio.wait_for(): - _upload_ciphertext: CDN upload (120s timeout) - _download_bytes: CDN download (configurable timeout) - _download_remote_media: remote media fetch (30s timeout) Also set total=None on _send_session to disable aiohttp built-in timeout, and change trust_env=True to False to bypass proxy for WeChat CDN connections.
… 3.13+ aiohttp 3.13+ uses aiohappyeyeballs for async DNS resolution in TCPConnector._resolve_host_with_throttle(). When send() is invoked via asyncio.run_coroutine_threadsafe() from worker threads (cron delivery, agent responses), internal Futures get attached to the wrong event loop, causing all WeChat message delivery to fail with: Task got Future attached to a different loop Fix: - Use ThreadedResolver in _make_ssl_connector() so DNS resolution happens in a thread pool instead of the buggy async path. - Add loop compatibility check in send_weixin_direct() to gracefully fall back when the session's loop doesn't match the running loop. Tested: cross-thread aiohttp request succeeds; WeChat messages deliver normally after restart.
liuhao1024
left a comment
There was a problem hiding this comment.
Good fix for the aiohttp 3.13+ event-loop / task-context issues. The asyncio.wait_for approach is well-reasoned and the ThreadedResolver change makes sense.
One concern: the PR also silently changes trust_env=True → False on all four ClientSession instances (lines 1028, 1241, 1248, and the _download_remote_media session at the bottom), but the PR description only mentions the event-loop/timeout fix.
Setting trust_env=False means environment proxy variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) will be silently ignored. Any Weixin deployment behind a corporate proxy that relies on env-var-based proxy configuration would lose connectivity with no error — just silent timeouts or connection refused.
This change is independent of the event-loop fix (the ThreadedResolver + explicit SSL context work regardless of trust_env). If the intent is to avoid proxy interference with Weixin's CDN endpoints, consider:
- Reverting
trust_envtoTrue(the real fix is inThreadedResolver+asyncio.wait_for), or - Adding an explicit comment explaining why
trust_env=Falseis required for this platform, plus a follow-up note in the PR description so operators are aware of the behavioral change.
|
Related: #13520, #12810, #19141 (merged) — prior fixes for cross-event-loop session reuse in Weixin adapter. This PR goes further by addressing aiohttp 3.13+ |
…on rate-limit fallback
Two bugs caused iLink errcode=-2 to be misidentified as genuine rate
limiting when it was actually a stale/expired session, leading to
infinite retries that always fail.
Bug 1: _is_stale_session_ret() only recognized errmsg "unknown error"
as a session-expiry signal, but iLink also returns empty strings,
"session expired", "token expired", etc. with errcode=-2. These
were all misclassified as rate-limit errors.
Before: (errmsg or "").lower() == "unknown error"
After: also match empty errmsg, "unknown error", and any string
containing "expire" (covers locale-dependent variants)
Bug 2: The rate-limit retry branch in _send_text_chunk() never cleared
context_token, so if errcode=-2 was actually a stale session that
slipped through _is_stale_session_ret, every retry would carry the
same expired token and always get -2 again — a dead loop until the
retry budget was exhausted.
Fix: strip context_token on the first rate-limit hit (with
retried_without_token guard), same as the explicit session-expired
branch. This provides a second safety net: even if the errmsg
doesn't match any known pattern, the degraded tokenless retry can
still succeed.
Together these changes ensure that stale iLink sessions are recovered
reliably regardless of the exact errmsg wording returned by the server.
|
Thanks for the detailed Weixin investigation. Current Problems
Suggested changes
Automated hermes-sweeper review. |
Problem
All WeChat (iLink) message delivery fails with:
This happens when
send()is invoked viaasyncio.run_coroutine_threadsafe()from worker threads — which covers all cron job deliveries and most agent responses through the gateway.Root Cause
aiohttp 3.13+ uses
aiohappyeyeballsfor async DNS resolution inTCPConnector._resolve_host_with_throttle(). Internally it creates Futures that can get bound to the wrong event loop when the coroutine is submitted cross-thread viarun_coroutine_threadsafe().This affects:
send()callFix
Two changes in
gateway/platforms/weixin.py:_make_ssl_connector(): Passresolver=aiohttp.resolver.ThreadedResolver()toTCPConnector. This routes DNS resolution through a thread pool instead of the buggy asyncaiohappyeyeballspath.send_weixin_direct(): Add loop compatibility check — comparesession._loopwithasyncio.get_running_loop()before using the gateway-loop session, and fall back gracefully when they mismatch.Testing
run_coroutine_threadsafe): ✅ passes