mirror of
https://github.com/open-webui/open-webui.git
synced 2026-05-18 05:05:09 +02:00
fix(mcp): remove asyncio.wait_for/shield from MCP cleanup in chat handler (#24105)
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. Signed-off-by: Adam Tao <tcx4c70@gmail.com> Co-authored-by: Tim Baek <tim@openwebui.com> Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com> Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com> Co-authored-by: Kylapaallikko <Kylapaallikko@users.noreply.github.com> Co-authored-by: Teay <pythontogoplease@gmail.com> Co-authored-by: tcx4c70 <tcx4c70@gmail.com> Co-authored-by: goodbey857 <76645482+goodbey857@users.noreply.github.com> Co-authored-by: Jacob Leksan <63938553+jmleksan@users.noreply.github.com> Co-authored-by: RomualdYT <romuald@gameurnews.fr> Co-authored-by: Lucas <lucas@vanosenbruggen.com> Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com> Co-authored-by: Constantine <Runixer@gmail.com> 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:
@@ -2040,25 +2040,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'):
|
||||
|
||||
Reference in New Issue
Block a user