mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-16 23:43:03 +00:00
fix(mcp): remove asyncio.wait_for/shield from MCP cleanup in chat handler
asyncio.wait_for() and asyncio.shield() create new asyncio Tasks which violate anyio cancel-scope task-ownership rules. The MCPClient's exit_stack contains anyio resources (streamable_http transport) that use anyio cancel scopes. When exited from a different task, anyio raises 'Attempted to exit a cancel scope that isn't the current task's current cancel scope' as a BaseException. This BaseException propagates through the finally block, discards the completed response return value, and surfaces as a 500 Internal Server Error / 'No response returned.' - silently swallowing successful MCP tool calls and blocking the chat endpoint. Fix: call client.disconnect() directly in a simple loop. MCPClient.disconnect() already catches BaseException internally (see prior commit), so no wrapper is needed. Co-Authored-By: Circe (Claude Code Sonnet 4.6) <circe@athena-council.org> Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8dae237a0b
commit
532c60fe8c
1 changed files with 15 additions and 12 deletions
|
|
@ -1919,25 +1919,28 @@ async def chat_completion(
|
|||
detail=error_detail,
|
||||
)
|
||||
finally:
|
||||
# MCP cleanup — MUST run in the SAME asyncio task as
|
||||
# connect() because the MCP SDK's streamablehttp_client
|
||||
# uses anyio task groups whose cancel scopes enforce
|
||||
# same-task exit. Do NOT wrap in asyncio.shield() or
|
||||
# asyncio.wait_for() — both create a new task.
|
||||
# Clean up MCP clients. Each client is isolated so one
|
||||
# failure doesn't skip the rest.
|
||||
#
|
||||
# NOTE: asyncio.wait_for() / asyncio.shield() must NOT be used
|
||||
# here — they create new asyncio Tasks, which violate anyio
|
||||
# cancel-scope task-ownership rules when the MCPClient's
|
||||
# exit_stack contains anyio transport resources (streamable_http).
|
||||
# Exiting those cancel scopes from the wrong task raises
|
||||
# "Attempted to exit a cancel scope that isn't the current
|
||||
# task's current cancel scope", which propagates as a
|
||||
# BaseException through the finally block, discards the response
|
||||
# return value, and surfaces as a 500 "No response returned."
|
||||
# MCPClient.disconnect() already catches BaseException internally.
|
||||
try:
|
||||
if mcp_clients := metadata.get('mcp_clients'):
|
||||
for client in reversed(list(mcp_clients.values())):
|
||||
try:
|
||||
await client.disconnect()
|
||||
except Exception as e:
|
||||
except BaseException as e:
|
||||
log.debug(f'Error disconnecting MCP client: {e}')
|
||||
except asyncio.CancelledError:
|
||||
# Let the client close asynchronously by GC
|
||||
pass
|
||||
except Exception as e:
|
||||
except BaseException as e:
|
||||
log.debug(f'Error cleaning up MCP clients: {e}')
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if metadata.get('chat_id'):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue