fix(weixin): replace per-request aiohttp ClientTimeout with asyncio.wait_for in _api_post/_api_get - #32179
fix(weixin): replace per-request aiohttp ClientTimeout with asyncio.wait_for in _api_post/_api_get#32179ryan-flow wants to merge 2 commits into
Conversation
…ait_for in _api_post/_api_get Fixes "Timeout context manager should be used inside a task" errors when send_message tool invokes weixin adapter via _run_async on a worker thread with a different event loop than the gateway's. asyncio.wait_for() creates its own Task on the current loop, avoiding aiohttp BaseTimeout's reliance on asyncio.current_task(loop=session._loop).
hclsys
left a comment
There was a problem hiding this comment.
The fix is correct and brings these two helpers in line with the existing house pattern — I verified the RCA and the consistency claim:
- The root cause is real:
aiohttp.ClientTimeoutbinds its timer to the session's loop, butsend_messageruns_api_post/_api_getin a worker thread with a different event loop (_run_async), so aiohttp'sasyncio.current_task(loop=self._loop)returnsNone→ "Timeout context manager should be used inside a task". - The fix is semantically equivalent:
aiohttp.ClientTimeout(total=X)is a whole-request deadline, and wrapping the entire_do_post()/_do_get()coroutine (includingawait response.text()) inasyncio.wait_for(..., timeout=X)covers connect+send+read the same way. On timeout,wait_forcancels the coroutine and theasync with session.post(...)__aexit__releases the connection, so no leak. - The consistency claim checks out:
_upload_ciphertext(gateway/platforms/weixin.py:554-568) and_download_bytes(:577) on main already use this exactwait_for+ inner-coroutine pattern with the same "avoid aiohttp ClientTimeout" comment. So this is bringing_api_post/_api_getin line with the established fix, not inventing a new approach.
One low-effort suggestion, not a blocker: there's no regression test. The bug is loop-context-specific and awkward to unit-test, and the sibling functions apparently lack one too — but since this failure mode has now recurred across multiple helpers, a small guard (e.g. running _api_post under a worker loop distinct from the session's and asserting it doesn't raise the "Timeout context manager…" error) would stop the next _api_* helper from quietly reintroducing aiohttp.ClientTimeout. Worth considering given it's bitten the file more than once.
Logic LGTM.
Tests that _api_post/_api_get use asyncio.wait_for (not aiohttp ClientTimeout) and work correctly when invoked from a different event loop via run_coroutine_threadsafe — the exact scenario that caused the original bug.
|
Added regression tests in
All tests use mocked aiohttp responses (no real HTTP calls). Ready for re-review. |
|
Understood, closing in favor of #31853. Quick note: #31853 doesn't include regression tests. If the maintainers are open to it, I'd like to contribute the test suite ( Alternatively, happy to open a separate test-only PR after #31853 is merged. Let me know what works best. |
Fix: "Timeout context manager should be used inside a task" in Weixin send
Problem
When the
send_messagetool sends files/media to WeChat via_run_async, a worker thread with its own event loop is created._api_postand_api_getuseaiohttp.ClientTimeoutper-request timeout, but aiohttp BaseTimeout callsasyncio.current_task(loop=self._loop)— whereself._loopis the gateway loop, not the worker loop — socurrent_task()returns None.Fix
Replace per-request
aiohttp.ClientTimeoutwithasyncio.wait_for(), consistent with existing_upload_ciphertextand_download_bytes.Scope
gateway/platforms/weixin.py_api_post,_api_get