mirror of
https://github.com/open-webui/open-webui.git
synced 2026-05-06 02:48:13 -05:00
[PR #24104] fix(mcp): fix verify endpoint 500 — replace asyncio.wait_for/shield with BaseException handler in MCPClient.disconnect() #43136
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/24104
Author: @looselyhuman
Created: 4/24/2026
Status: 🔄 Open
Base:
dev← Head:gaia-patch-1📝 Commits (10+)
fe6783cMerge pull request #19030 from open-webui/devfc05e0aMerge pull request #19405 from open-webui/deve3faec6Merge pull request #19416 from open-webui/dev9899293Merge pull request #19448 from open-webui/dev140605eMerge pull request #19462 from open-webui/dev6f1486fMerge pull request #19466 from open-webui/devd95f533Merge pull request #19729 from open-webui/deva7271530.6.43 (#20093)6adde20Merge pull request #20394 from open-webui/devf9b0534Merge pull request #20522 from open-webui/dev📊 Changes
1 file changed (+22 additions, -17 deletions)
View changed files
📝
backend/open_webui/utils/mcp/client.py(+22 -17)📄 Description
Pull Request Checklist
devbranch.dev.fixprefix used.Problem
Calling the MCP verify endpoint (or any path that triggers
MCPClient.disconnect()) intermittently returns HTTP 500 with an empty/partial manifest. The error trace shows aBaseExceptionGroupescaping, which Starlette catches and converts to a 500 response.Root Cause
The previous
disconnect()implementation wrappedexit_stack.aclose()withasyncio.wait_for(asyncio.shield(...)). Bothasyncio.wait_for()andasyncio.shield()create a new asyncio Task to run the coroutine. TheMCPClient's exit stack holds anyio resources (thestreamable_httptransport) that use anyio cancel scopes.anyio cancel scopes are owned by the task that entered them. When
exit_stack.aclose()runs in a different task (the one created bywait_for/shield), anyio raises:This is a
BaseException, not anException, so it bypasses theexcept Exceptionhandler and escapes as aBaseExceptionGroup→ Starlette returns 500.Fix
asyncio.wait_for/asyncio.shieldwrapper — callexit_stack.aclose()directly in the original task.except Exceptiontoexcept BaseExceptionso any remaining errors from the anyio transport internals (cancelled generators, cancel scope mismatches) are caught and logged rather than propagated.asyncio.wait_for,anyio.fail_after, andasyncio.shieldare all unsafe in this context.Changelog Entry
Description
Bug fix for
MCPClient.disconnect()causing 500 errors on the MCP verify endpoint when using anyio-based transports.Fixed
MCPClient.disconnect()no longer spawns a child task viaasyncio.wait_for/asyncio.shield, which caused anyio cancel scope ownership violations and intermittent 500 responses on the verify endpoint.except Exceptiontoexcept BaseExceptionto catch anyio transport cleanup errors that would otherwise escape.Testing
I tested this on my self-hosted OpenWebUI instance running behind a Cloudflare reverse proxy tunnel. I connected an MCP server using FastMCP with stateless HTTP transport, authenticated via Bearer token, through the Admin → Tool Servers UI. Before this fix, clicking "Verify" on the tool server would intermittently return 500 with an empty or partial manifest. After applying this fix, the verify endpoint consistently returns 200 with all tool schemas populated. I repeated the verify operation multiple times to confirm the fix was stable.
Contributor License Agreement
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.