fix(frontend): handle websocket disconnect issue (#11144)

## Changes 🏗️

I found that if I logged out while an agent was running, sometimes
Webscokets would keep open connections but fail to connect ( given there
is no token anymore ) and cause strange behavior down the line on the
login screen.

Two root causes behind after inspecting the browser logs 🧐 
- WebSocket connections were attempted with an empty token right after
logout, yielding `wss://.../ws?token=` and repeated `1006/connection`
refused loops.
- During logout, sockets in `CONNECTING` state weren’t being closed, so
the browser kept trying to finish the handshake and were reattempted
shortly after failing

Trying to fix this like:
- Guard `connectWebSocket()` to no-op if a logout/disconnect intent is
set, and to skip connecting when no token is available.
- Treat `CONNECTING` sockets as closeable in `disconnectWebSocket()` and
clear `wsConnecting` to avoid stale pending Promises
- Left existing heartbeat/reconnect logic intact, but it now won’t run
when we’re logging out or when we can’t get a token.

### Checklist 📋

#### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Login and run an agent that takes long to run
  - [x] Logout
  - [x] Check the browser console and you don't see any socket errors
  - [x] The login screen behaves ok   

### For configuration changes:

Noop
This commit is contained in:
Ubbe
2025-10-13 16:10:16 +04:00
committed by GitHub
parent f67d78df3e
commit 99ac206272

View File

@@ -1126,6 +1126,11 @@ export default class BackendAPI {
}
async connectWebSocket(): Promise<void> {
// Do not attempt to connect if a disconnect intent is present (e.g., during logout)
if (this._hasDisconnectIntent()) {
return;
}
this.isIntentionallyDisconnected = false;
return (this.wsConnecting ??= new Promise(async (resolve, reject) => {
try {
@@ -1139,7 +1144,19 @@ export default class BackendAPI {
}
} catch (error) {
console.warn("Failed to get token for WebSocket connection:", error);
// Continue with empty token, connection might still work
// Intentionally fall through; we'll bail out below if no token is available
}
// If we don't have a token, skip attempting a connection.
if (!token) {
console.info(
"[BackendAPI] Skipping WebSocket connect: no auth token available",
);
// Resolve first, then clear wsConnecting to avoid races for awaiters
resolve();
this.wsConnecting = null;
this.webSocket = null;
return;
}
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
@@ -1178,6 +1195,9 @@ export default class BackendAPI {
if (!wasIntentional) {
this.wsOnDisconnectHandlers.forEach((handler) => handler());
setTimeout(() => this.connectWebSocket().then(resolve), 1000);
} else {
// Ensure pending connect calls settle on intentional close
resolve();
}
};
@@ -1197,9 +1217,14 @@ export default class BackendAPI {
disconnectWebSocket() {
this.isIntentionallyDisconnected = true;
this._stopWSHeartbeat(); // Stop heartbeat when disconnecting
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
if (
this.webSocket &&
(this.webSocket.readyState === WebSocket.OPEN ||
this.webSocket.readyState === WebSocket.CONNECTING)
) {
this.webSocket.close();
}
this.wsConnecting = null;
}
private _hasDisconnectIntent(): boolean {