From b952e404fa253d1ea3ea86252d1951a068c158d5 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 29 Mar 2026 20:54:21 -0400 Subject: [PATCH] Control UI: clear queued connect timeout on stop (#57338) Merged via squash. Prepared head SHA: a359fe836743019211ce817d8e32eca09d236e41 Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 1 + ui/src/ui/gateway.node.test.ts | 43 ++++++++++++++++++++++++++++++---- ui/src/ui/gateway.ts | 4 ++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13dac6248cc..e6e3d5c6e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -227,6 +227,7 @@ Docs: https://docs.openclaw.ai - Gateway/SQLite transient handling: keep unhandled `SQLITE_CANTOPEN`, `SQLITE_BUSY`, `SQLITE_LOCKED`, and `SQLITE_IOERR` failures non-fatal in the global rejection handler so macOS LaunchAgent restarts do not enter a crash-throttle loop. (#57018) - Control UI/gateway: reconnect the browser client when gateway event sequence gaps are detected, so stale non-chat state recovers automatically instead of only telling the user to refresh. (#23912) thanks @Olshansk. - ClawDock/docs: move the helper scripts to `scripts/clawdock`, publish ClawDock as a first-class docs page on the docs site, and document reinstalling local helper copies from the new raw GitHub path. (#23912) thanks @Olshansk. +- Control UI/gateway: clear queued browser connect timeouts on client stop so aborted or replaced gateway clients do not send delayed connect requests after shutdown. (#57338) thanks @gumadeiras. ## 2026.3.24 diff --git a/ui/src/ui/gateway.node.test.ts b/ui/src/ui/gateway.node.test.ts index 5f894ad548d..5eafa8314f5 100644 --- a/ui/src/ui/gateway.node.test.ts +++ b/ui/src/ui/gateway.node.test.ts @@ -92,6 +92,16 @@ type ConnectFrame = { }; }; +function stubWindowGlobals(storage?: ReturnType) { + vi.stubGlobal("window", { + location: { href: "http://127.0.0.1:18789/" }, + localStorage: storage, + setTimeout: (handler: (...args: unknown[]) => void, timeout?: number, ...args: unknown[]) => + globalThis.setTimeout(() => handler(...args), timeout), + clearTimeout: (timeoutId: number | undefined) => globalThis.clearTimeout(timeoutId), + }); +} + function getLatestWebSocket(): MockWebSocket { const ws = wsInstances.at(-1); if (!ws) { @@ -182,10 +192,7 @@ describe("GatewayBrowserClient", () => { }); vi.stubGlobal("localStorage", storage); - Object.defineProperty(window, "localStorage", { - configurable: true, - value: storage, - }); + stubWindowGlobals(storage); localStorage.clear(); vi.stubGlobal("WebSocket", MockWebSocket); @@ -377,6 +384,26 @@ describe("GatewayBrowserClient", () => { vi.useRealTimers(); }); + it("cancels a queued connect send when stopped before the timeout fires", async () => { + vi.useFakeTimers(); + + const client = new GatewayBrowserClient({ + url: "ws://127.0.0.1:18789", + token: "shared-auth-token", + }); + + client.start(); + const ws = getLatestWebSocket(); + ws.emitOpen(); + + client.stop(); + await vi.advanceTimersByTimeAsync(750); + + expect(ws.sent).toHaveLength(0); + + vi.useRealTimers(); + }); + it("does not auto-reconnect on AUTH_TOKEN_MISSING", async () => { vi.useFakeTimers(); localStorage.clear(); @@ -408,6 +435,14 @@ describe("GatewayBrowserClient", () => { }); describe("shouldRetryWithDeviceToken", () => { + beforeEach(() => { + stubWindowGlobals(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + it("allows a bounded retry for trusted loopback endpoints", () => { expect( shouldRetryWithDeviceToken({ diff --git a/ui/src/ui/gateway.ts b/ui/src/ui/gateway.ts index 00f63a37767..cb6bfce66ce 100644 --- a/ui/src/ui/gateway.ts +++ b/ui/src/ui/gateway.ts @@ -294,6 +294,10 @@ export class GatewayBrowserClient { stop() { this.closed = true; + if (this.connectTimer !== null) { + window.clearTimeout(this.connectTimer); + this.connectTimer = null; + } this.ws?.close(); this.ws = null; this.pendingConnectError = undefined;