fix(workspace): normalize websocket headers for reverse proxies and show detailed disconnect reason
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m4s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m4s
This commit is contained in:
1 parent
c2ed76c6a4
commit
155d2daa02
3 files changed
+49
-7
No files matched your search
@@ -134,6 +134,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const wsReconnectTimerRef = useRef<any>(null);
|
const wsReconnectTimerRef = useRef<any>(null);
|
||||||
const wsReconnectDelayRef = useRef<number>(2000);
|
const wsReconnectDelayRef = useRef<number>(2000);
|
||||||
const [wsConnected, setWsConnected] = useState<boolean>(false);
|
const [wsConnected, setWsConnected] = useState<boolean>(false);
|
||||||
|
const [wsDisconnectReason, setWsDisconnectReason] = useState<string | null>(null);
|
||||||
|
|
||||||
// Folder state (synchronized with server)
|
// Folder state (synchronized with server)
|
||||||
const [customFolders, setCustomFolders] = useState<string[]>([]);
|
const [customFolders, setCustomFolders] = useState<string[]>([]);
|
||||||
@@ -972,22 +973,37 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
wsReconnectDelayRef.current = 2000;
|
wsReconnectDelayRef.current = 2000;
|
||||||
|
setWsDisconnectReason(null);
|
||||||
setWsConnected(true);
|
setWsConnected(true);
|
||||||
api.getFolders().then(setCustomFolders).catch(() => {});
|
api.getFolders().then(setCustomFolders).catch(() => {});
|
||||||
api.getSharedFolders().then(setSharedFolders).catch(() => {});
|
api.getSharedFolders().then(setSharedFolders).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = (evt: CloseEvent) => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
setWsConnected(false);
|
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);
|
clearTimeout(wsReconnectTimerRef.current);
|
||||||
const delay = wsReconnectDelayRef.current;
|
const delay = wsReconnectDelayRef.current;
|
||||||
wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5));
|
wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5));
|
||||||
wsReconnectTimerRef.current = setTimeout(connect, delay);
|
wsReconnectTimerRef.current = setTimeout(connect, delay);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = (e) => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
console.warn('[MindSift Workspace WS] Fehler aufgetreten:', e);
|
||||||
setWsConnected(false);
|
setWsConnected(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -997,9 +1013,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
handleWsMsg(msg);
|
handleWsMsg(msg);
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
setWsConnected(false);
|
setWsConnected(false);
|
||||||
|
const errText = err?.message || 'Verbindungsfehler';
|
||||||
|
setWsDisconnectReason(errText);
|
||||||
clearTimeout(wsReconnectTimerRef.current);
|
clearTimeout(wsReconnectTimerRef.current);
|
||||||
const delay = wsReconnectDelayRef.current;
|
const delay = wsReconnectDelayRef.current;
|
||||||
wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5));
|
wsReconnectDelayRef.current = Math.min(30000, Math.round(wsReconnectDelayRef.current * 1.5));
|
||||||
@@ -1009,6 +1027,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
connect();
|
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'
|
// Fallback: also listen for window custom event 'mindsift:ws'
|
||||||
const handleGlobalCustomEvent = (e: any) => {
|
const handleGlobalCustomEvent = (e: any) => {
|
||||||
if (e.detail) {
|
if (e.detail) {
|
||||||
@@ -1019,6 +1046,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
|
unsubGlobalStatus();
|
||||||
clearTimeout(wsReconnectTimerRef.current);
|
clearTimeout(wsReconnectTimerRef.current);
|
||||||
if (wsRef.current) {
|
if (wsRef.current) {
|
||||||
try {
|
try {
|
||||||
@@ -4050,7 +4078,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
title={
|
title={
|
||||||
wsConnected
|
wsConnected
|
||||||
? 'Live synchronisiert (WebSocket online)'
|
? 'Live synchronisiert (WebSocket online)'
|
||||||
: 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv'
|
: `Proxy/Offline-Fallback: Synchronisation über HTTP aktiv${wsDisconnectReason ? ` (${wsDisconnectReason})` : ''}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : 'opacity-80'}`} />
|
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : 'opacity-80'}`} />
|
||||||
@@ -4090,7 +4118,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
title={
|
title={
|
||||||
wsConnected
|
wsConnected
|
||||||
? 'Live synchronisiert (WebSocket online)'
|
? 'Live synchronisiert (WebSocket online)'
|
||||||
: 'Proxy/Offline-Fallback: Synchronisation über HTTP aktiv'
|
: `Proxy/Offline-Fallback: Synchronisation über HTTP aktiv${wsDisconnectReason ? ` (${wsDisconnectReason})` : ''}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : 'opacity-80'}`} />
|
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : 'opacity-80'}`} />
|
||||||
|
|||||||
@@ -1118,8 +1118,9 @@ export function connectWebSocket(): void {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = (evt: CloseEvent) => {
|
||||||
activeGlobalWS = null;
|
activeGlobalWS = null;
|
||||||
|
console.warn('[MindSift Global WS] Closed with code:', evt?.code, 'reason:', evt?.reason);
|
||||||
notifyStatusChange(false);
|
notifyStatusChange(false);
|
||||||
if (!manualDisconnect && getToken()) {
|
if (!manualDisconnect && getToken()) {
|
||||||
clearTimeout(wsReconnectTimer);
|
clearTimeout(wsReconnectTimer);
|
||||||
@@ -1131,7 +1132,8 @@ export function connectWebSocket(): void {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = (e) => {
|
||||||
|
console.warn('[MindSift Global WS] Error:', e);
|
||||||
notifyStatusChange(false);
|
notifyStatusChange(false);
|
||||||
try {
|
try {
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|||||||
@@ -1585,6 +1585,18 @@ func (s *Server) handleReorderListItems(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleWebSocket(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")
|
roomID := chi.URLParam(r, "listId")
|
||||||
if roomID == "" {
|
if roomID == "" {
|
||||||
roomID = chi.URLParam(r, "spaceId")
|
roomID = chi.URLParam(r, "spaceId")
|
||||||
|
|||||||
Reference in new issue
Block a user