fix: add pong/timeout detection to prevent silent connection hangs
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 2m2s

- Backend now responds to __ping__ with __pong__
- Frontend detects missing pong within 5s and closes connection
- This triggers proper reconnect/auth recovery instead of hanging
- Fixes 'keepalive ping timeout' issue where connection appears alive but is dead
This commit is contained in:
Gan, Jimmy
2026-03-11 23:55:51 +08:00
parent 74100c8882
commit e0f432bc39
2 changed files with 26 additions and 1 deletions
+1
View File
@@ -180,6 +180,7 @@ async def ws_endpoint(websocket: WebSocket, host: str = Query("nas"), token: str
process.stdin.write(data) process.stdin.write(data)
elif "text" in msg and msg["text"]: elif "text" in msg and msg["text"]:
if msg["text"] == "__ping__": if msg["text"] == "__ping__":
await websocket.send_text("__pong__")
continue continue
process.stdin.write(msg["text"].encode()) process.stdin.write(msg["text"].encode())
except Exception as e: except Exception as e:
+25 -1
View File
@@ -190,18 +190,24 @@
const proto = location.protocol === "https:" ? "wss:" : "ws:"; const proto = location.protocol === "https:" ? "wss:" : "ws:";
let heartbeat = null; let heartbeat = null;
let pongTimeout = null;
let reconnectTimer = null; let reconnectTimer = null;
let closedManually = false; let closedManually = false;
let reconnecting = false; let reconnecting = false;
let authRecoveryInFlight = false; let authRecoveryInFlight = false;
let reconnectAttempts = 0; let reconnectAttempts = 0;
const MAX_RECONNECT_DELAY = 30000; const MAX_RECONNECT_DELAY = 30000;
const PONG_TIMEOUT = 5000; // 5s to receive pong
function clearHeartbeat() { function clearHeartbeat() {
if (heartbeat) { if (heartbeat) {
clearInterval(heartbeat); clearInterval(heartbeat);
heartbeat = null; heartbeat = null;
} }
if (pongTimeout) {
clearTimeout(pongTimeout);
pongTimeout = null;
}
} }
function handleAuthExpired(reason = "Session expired — please log in again") { function handleAuthExpired(reason = "Session expired — please log in again") {
@@ -280,7 +286,15 @@
sendSize(ws); sendSize(ws);
clearHeartbeat(); clearHeartbeat();
heartbeat = setInterval(() => { heartbeat = setInterval(() => {
if (ws.readyState === 1) ws.send("__ping__"); if (ws.readyState === 1) {
ws.send("__ping__");
// Set timeout to detect missing pong
pongTimeout = setTimeout(() => {
if (ws.readyState === 1) {
ws.close(1000, "keepalive ping timeout");
}
}, PONG_TIMEOUT);
}
}, 12000); }, 12000);
const d = tabData.get(tabId); const d = tabData.get(tabId);
if (d) { if (d) {
@@ -293,6 +307,16 @@
} }
}; };
ws.onmessage = (e) => { ws.onmessage = (e) => {
// Check for text pong response
if (typeof e.data === 'string' && e.data === '__pong__') {
// Clear the pong timeout - connection is alive
if (pongTimeout) {
clearTimeout(pongTimeout);
pongTimeout = null;
}
return;
}
const data = new Uint8Array(e.data); const data = new Uint8Array(e.data);
const isPersistenceMessage = tab.persistent const isPersistenceMessage = tab.persistent
&& data.length >= PERSISTENCE_STATUS_PREFIX_BYTES.length && data.length >= PERSISTENCE_STATUS_PREFIX_BYTES.length