mirror of
https://github.com/open-webui/terminals.git
synced 2026-11-05 01:37:21 +00:00
[GH-ISSUE #58] bug: Embedded terminal shows "connection closed" in Per chat context mode — orchestrator WS dial omits the chat-scoped x-session-id ownership header (Shared mode unaffected) #29
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
open-webui/terminals#29
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
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?
Originally created by @Tchipsy on GitHub (Sep 13, 2026).
Original GitHub issue: https://github.com/open-webui/terminals/issues/58
Description
With an orchestrator deployment (
terminals+ per-sessionopen-terminalcontainers, Docker backend), the embedded terminal panel in Open WebUI works in Terminal Contexts "Shared" mode but never connects in "Per chat" mode: the xterm stays completely black, and typing anything surfaces "connection closed".Everything else works in Per chat mode (model tools, file browser) — only the interactive WebSocket terminal dies, and only in Per chat.
Root cause: the session container's WebSocket ownership check compares the
chat_idstored at session-creation time against the WS handshake — and the orchestrator's WS dial does not forward thex-session-idheader that the session-creating POST carried. In Per chat mode that header is present (non-empty), so the check always fails with4004 "Session not found". In Shared mode the POST carries neither ownership header, so the check passes.Reproduction
Expected behavior
The embedded terminal connects in every Terminal Contexts mode. The orchestrator should present the same ownership identity to the session container on the WebSocket dial that was used when the session was created.
Actual behavior
Per chat mode only: the three-leg WebSocket chain establishes (browser → open-webui → terminals → container all log
[accepted]/connection open), then the container closes the last leg with4004 "Session not found"during its post-auth ownership check. The close is relayed back to the browser ("connection closed"). The container sends no PTY output first, hence the black screen.Root cause analysis
proxy_terminalinjectsX-User-Id, and the browser sendsx-session-idin Per chat mode (chat-scoped cwd tracking). The orchestrator's_proxy_request(routers/proxy.py) stripsx-user-idfrom the forwarded headers but forwardsx-session-idunchanged: The container'screate_terminal()therefore storesuser_id=""andchat_id="<non-empty in Per chat mode>"._ws_proxy()dials the container with and its first-message auth carries only{"type": "auth", "token": instance.api_key}— nox-session-idhandshake header and nochat_idin the payload.open_terminal/main.py,ws_terminal): In Per chat mode the storedchat_idis non-empty → always fails. In Shared mode the POST carried neither header → both stored values are""→ passes. This explains the mode asymmetry exactly.The identity the session was created with is never re-asserted on the WebSocket dial.
Suggested fix direction
verify_user_id, and the forwardedx-session-id) and re-assert it on the WS dial:connection openat accept time (before auth/ownership), and the orchestrator logs the upstream close only at debug level — a4004close is invisible at INFO on both sides. Logging upstream WS close code/reason at INFO would have made this diagnosable in minutes.Environment
ghcr.io/open-webui/terminals:main(revisione15903af)ghcr.io/open-webui/open-terminal:latest(revision72fb783b)v0.11.3(integration peer)TERMINALS_BACKEND=docker, Docker 29.8.0, Docker Compose v5.5.1Logs
All legs establish; the close is invisible at INFO level:
The actual close (
4004 "Session not found", proven by the reproducer above) never reaches the visible logs.Notes
/files/serve404, Windows path corruption) and terminals#36 (prior fix for the same class: a proxy path omitting a context header) are the closest, neither matches.files/servefalls back to thedefaultcontext in Per chat mode, breaking HTML preview) — independent endpoint, filed separately.Confirmation