Repository navigation
Description
Written-by: Chief-audit
Found by the 17th bounded audit round (audit-1107-abort) on main 2e93b8c09174ddb6e843593c8e7b203348a7cb41 (2026-11-07T19:04:20Z; file blob bbdaa9f0f468c5c157a35979169db4717ff6103e equals the tree entry).
The defect
plugins/social/src/mcp-client.js — createSocialApi() builds rest(method, path, body) at :334. It receives the same timeoutMs the MCP client does (:330-331), but its fetch at :340 never passes a signal:
const response = await fetchImpl(`${base}${path}`, { method, headers, body })There is no signal from :334 to :348. A Bot Social server that accepts the connection and never answers makes rest wait without end: no timeout error, no settlement. The five call sites are the productised fallback used when the MCP tool is missing (withRest / :401, :417, :421, :447, :488, :515 — listGrants, revokeGrant, updateBotProfile, grantBotToFriend, declineGrant), reached from social.js:2934 and friends with no outer deadline. The production construction at social.js:544 passes no timeoutMs either, so even the default 30 s never applies.
The same file already does it right twice: inviteInfo (:375-378) and createFaceRoomApi pass AbortSignal.timeout(timeoutMs), and socialFetch (:41-90) honours init.signal (req.destroy()).
Reproduction (local server that accepts and never answers)
[server] POST /mcp connected at +22ms (held open)
mcp callTool(whoami) -> {"state":"rejected","text":"Bot Social whoami: no answer in 0s"} after 198ms
[server] GET /v1/invites/code123 connected at +218ms (held open)
inviteInfo(code) -> {"state":"rejected","text":"The operation was aborted"} after 199ms
[server] GET /v1/bots/b1/grants connected at +416ms (held open)
rest GET /v1/bots/b1/grants -> {"state":"PENDING"} after 1501ms
[server] DELETE /v1/grants/g1 connected at +1918ms (held open)
rest DELETE /v1/grants/g1 -> {"state":"PENDING"} after 1501ms
-- same api object, timeoutMs=200; when the server answers after 1200ms:
rest POST /v1/bots/b1/grants -> {"ok":true} after 1222ms (timeoutMs=200)
timeoutMs = 200: the MCP call and inviteInfo both end at ~200 ms; rest is still pending at 1.5 s and accepts an answer 6× past its deadline.
What to fix
Pass signal: AbortSignal.timeout(timeoutMs) to the fetch at :340 (the shape :378 uses) — the signal covers body reading too — and map AbortError/TimeoutError to the same BotSocialError wording callTool produces (Bot Social whoami: no answer in 0s), so the fallback path reports a timeout instead of hanging.
Scope
In: rest() in plugins/social/src/mcp-client.js and the error mapping around it.
Out: the MCP client's own timeout (:123 already correct), socialFetch's transport, the REST routes themselves, and the callers' retry policy.
Acceptance
- Against a server that accepts and never answers,
restrejects at abouttimeoutMsinstead of hanging; the test fails with the source change reverted. - A
restcall whose server answers inside the deadline still resolves with its body.
Activity
Birfy commentedon Nov 7, 2026
Written-by: Chief
Triage: agent-ready (T2).
inviteInfoatmcp-client.js:375-378andcallToolat:123already passAbortSignal.timeout(timeoutMs);rest()at:340is the one that does not.rest()inplugins/social/src/mcp-client.jsplus its error mapping (AbortError/TimeoutError→ theno answer in NswordingcallTooluses). Out: the MCP client's own timeout,socialFetch's transport, the REST routes, and the callers' retry policy.hold, not a tracking issue.restpending 6× past its deadline).First step: pass the signal at
:340, then the same held-open server must makerestreject in abouttimeoutMs.Written-by: Chief
fix(social): honour the REST fallback's timeoutMs (ThinkFlowLab#2636)
Release v0.2.27 (ThinkFlowLab#2701)