From 02b65ea582eba9f97520c1790de3902dfdce605b Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:10:04 +0200 Subject: [PATCH] fix: don't hang terminal proxy when one forwarding pump exits first (#25479) The proxy gathered _client_to_upstream and _upstream_to_client with return_exceptions=True. When upstream sends a graceful CLOSE, _upstream_to_client returns but gather keeps waiting on _client_to_upstream, which is blocked in ws.receive() until the browser disconnects. The handler stays pending and the finally: session.close() cleanup is deferred, leaking a ClientSession and an open browser socket. Use asyncio.wait(return_when=FIRST_COMPLETED) and cancel the pending sibling, so the proxy unwinds as soon as either direction finishes. The pumps' bare except Exception already lets CancelledError (a BaseException) propagate, so cancellation is clean and they need no change. Fixes #25464 Co-authored-by: Claude Opus 4.8 (1M context) --- backend/open_webui/routers/terminals.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index 4c61d6ec1d..6a942cf9b2 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -331,11 +331,20 @@ async def ws_terminal( except Exception: pass - await asyncio.gather( - _client_to_upstream(), - _upstream_to_client(), - return_exceptions=True, - ) + # End the proxy as soon as either direction finishes (e.g. a + # graceful upstream CLOSE) and cancel the sibling, which would + # otherwise hang on a blocked ws.receive() until the browser leaves. + tasks = [ + asyncio.create_task(_client_to_upstream()), + asyncio.create_task(_upstream_to_client()), + ] + _done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass except Exception as e: log.exception('Terminal WebSocket proxy error: %s', e) finally: