From 155d2daa020b4a976da20ad9469d7d47ee749e7b Mon Sep 17 00:00:00 2001 From: Sandro Date: Mon, 26 Oct 2026 12:06:55 +0100 Subject: [PATCH] fix(workspace): normalize websocket headers for reverse proxies and show detailed disconnect reason --- .../src/features/workspace/WorkspaceView.tsx | 38 ++++++++++++++++--- frontend/src/lib/api.ts | 6 ++- internal/api/server.go | 12 ++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 8b5eb78..80143db 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -134,6 +134,7 @@ export const WorkspaceView: React.FC = ({ const wsReconnectTimerRef = useRef(null); const wsReconnectDelayRef = useRef(2000); const [wsConnected, setWsConnected] = useState(false); + const [wsDisconnectReason, setWsDisconnectReason] = useState(null); // Folder state (synchronized with server) const [customFolders, setCustomFolders] = useState([]); @@ -972,22 +973,37 @@ export const WorkspaceView: React.FC = ({ ws.onopen = () => { if (!isMounted) return; wsReconnectDelayRef.current = 2000; + setWsDisconnectReason(null); setWsConnected(true); api.getFolders().then(setCustomFolders).catch(() => {}); api.getSharedFolders().then(setSharedFolders).catch(() => {}); }; - ws.onclose = () => { + ws.onclose = (evt: CloseEvent) => { if (!isMounted) return; setWsConnected(false); + const reasonText = + evt.reason || + (evt.code === 1006 + ? 'Verbindung durch Firmen-Proxy oder Netzwerk getrennt (Code 1006)' + : evt.code === 1008 + ? 'Richtlinien-Verletzung durch Proxy/Firewall (Code 1008)' + : evt.code === 1002 + ? 'Protokollfehler beim Handshake (Code 1002)' + : evt.code === 1000 + ? 'Normal geschlossen (Code 1000)' + : `Code ${evt.code}`); + console.warn('[MindSift Workspace WS] getrennt:', evt.code, evt.reason || reasonText); + setWsDisconnectReason(reasonText); clearTimeout(wsReconnectTimerRef.current); const delay = wsReconnectDelayRef.current; wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5)); wsReconnectTimerRef.current = setTimeout(connect, delay); }; - ws.onerror = () => { + ws.onerror = (e) => { if (!isMounted) return; + console.warn('[MindSift Workspace WS] Fehler aufgetreten:', e); setWsConnected(false); }; @@ -997,9 +1013,11 @@ export const WorkspaceView: React.FC = ({ handleWsMsg(msg); } catch {} }; - } catch (err) { + } catch (err: any) { if (!isMounted) return; setWsConnected(false); + const errText = err?.message || 'Verbindungsfehler'; + setWsDisconnectReason(errText); clearTimeout(wsReconnectTimerRef.current); const delay = wsReconnectDelayRef.current; wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5)); @@ -1009,6 +1027,15 @@ export const WorkspaceView: React.FC = ({ connect(); + // Also synchronize with global WebSocket status + const unsubGlobalStatus = onWebSocketStatus((connected) => { + if (!isMounted) return; + if (connected) { + setWsConnected(true); + setWsDisconnectReason(null); + } + }); + // Fallback: also listen for window custom event 'mindsift:ws' const handleGlobalCustomEvent = (e: any) => { if (e.detail) { @@ -1019,6 +1046,7 @@ export const WorkspaceView: React.FC = ({ return () => { isMounted = false; + unsubGlobalStatus(); clearTimeout(wsReconnectTimerRef.current); if (wsRef.current) { try { @@ -4050,7 +4078,7 @@ export const WorkspaceView: React.FC = ({ title={ wsConnected ? 'Live synchronisiert (WebSocket online)' - : 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv' + : `Proxy/Offline-Fallback: Synchronisation über HTTP aktiv${wsDisconnectReason ? ` (${wsDisconnectReason})` : ''}` } > @@ -4090,7 +4118,7 @@ export const WorkspaceView: React.FC = ({ title={ wsConnected ? 'Live synchronisiert (WebSocket online)' - : 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv' + : `Proxy/Offline-Fallback: Synchronisation über HTTP aktiv${wsDisconnectReason ? ` (${wsDisconnectReason})` : ''}` } > diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 937e1c9..13f2508 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1118,8 +1118,9 @@ export function connectWebSocket(): void { } }; - ws.onclose = () => { + ws.onclose = (evt: CloseEvent) => { activeGlobalWS = null; + console.warn('[MindSift Global WS] Closed with code:', evt?.code, 'reason:', evt?.reason); notifyStatusChange(false); if (!manualDisconnect && getToken()) { clearTimeout(wsReconnectTimer); @@ -1131,7 +1132,8 @@ export function connectWebSocket(): void { } }; - ws.onerror = () => { + ws.onerror = (e) => { + console.warn('[MindSift Global WS] Error:', e); notifyStatusChange(false); try { ws.close(); diff --git a/internal/api/server.go b/internal/api/server.go index 012a9a9..d7993fe 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -1585,6 +1585,18 @@ func (s *Server) handleReorderListItems(w http.ResponseWriter, r *http.Request) } func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { + // Some corporate proxies and reverse proxies (e.g. OpenResty/Nginx or HTTP/2 intermediaries) + // strip or alter the Connection header (e.g. "close" or missing) while passing + // Sec-WebSocket-Key or Upgrade: websocket. Normalize it so Gorilla Upgrader succeeds. + if r.Header.Get("Sec-Websocket-Key") != "" { + if !strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") { + r.Header.Set("Connection", "Upgrade") + } + if !strings.Contains(strings.ToLower(r.Header.Get("Upgrade")), "websocket") { + r.Header.Set("Upgrade", "websocket") + } + } + roomID := chi.URLParam(r, "listId") if roomID == "" { roomID = chi.URLParam(r, "spaceId")