fix(kanban): treat dashboard event-stream cancellation as normal shutdown

Stopping `hermes dashboard` with Ctrl-C while the Kanban dashboard is
open prints an ASGI traceback ending in
`plugins/kanban/dashboard/plugin_api.py::stream_events` at the
`asyncio.sleep(_EVENT_POLL_SECONDS)` line. This is a normal shutdown
path: Uvicorn cancels the open websocket task while it is sleeping in
the 300 ms poll loop. `asyncio.CancelledError` is a `BaseException` in
Python 3.8+ — the bare `except Exception:` handler below the existing
`WebSocketDisconnect:` clause does NOT catch it, so the cancellation
surfaces as an application traceback and routine dashboard exit looks
like a runtime failure.

Add an explicit `except asyncio.CancelledError: return` clause beside
the existing `WebSocketDisconnect` handler. Disconnection (client
closed the tab) and shutdown cancellation (dashboard process exiting)
are conceptually different paths but both warrant a quiet return; the
two clauses are kept separate to keep that intent explicit.

`asyncio` is already imported and used in this scope, so no new
import is needed. The bare `except Exception:` handler is preserved
verbatim, so genuine runtime failures still log a warning and close
the socket cleanly.

Closes #20790.
This commit is contained in:
SandroHub013 2026-05-07 01:11:28 +02:00 committed by Teknium
parent 43a6645718
commit 36ad97337a

View file

@ -1521,6 +1521,13 @@ async def stream_events(ws: WebSocket):
await asyncio.sleep(_EVENT_POLL_SECONDS)
except WebSocketDisconnect:
return
except asyncio.CancelledError:
# Normal shutdown path: dashboard process exit (Ctrl-C) cancels the
# websocket task while it is sleeping in the poll loop.
# CancelledError is a BaseException in 3.8+ so the bare Exception
# handler below would not catch it; without this clause Uvicorn
# surfaces the cancellation as an application traceback. Quiet it.
return
except Exception as exc: # defensive: never crash the dashboard worker
log.warning("Kanban event stream error: %s", exc)
try: