From 5c97107c7ace6d39efb1d1ac1ca27030e2af3da3 Mon Sep 17 00:00:00 2001 From: Alix-007 Date: Thu, 16 Jul 2026 09:51:42 +0800 Subject: [PATCH] fix(ui): bound mount recovery probes (#108163) Co-authored-by: Peter Steinberger --- ui/index.html | 13 +++++++++++- ui/src/app/mount-fallback.test.ts | 35 ++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/ui/index.html b/ui/index.html index 03fdff27b9f9..2c3d2abcb8ec 100644 --- a/ui/index.html +++ b/ui/index.html @@ -339,14 +339,25 @@ recoveryInFlight = true; recoveryAttempt += 1; documentUrl.searchParams.set("openclaw_mount_recovery", String(Date.now())); + var recoveryController = new AbortController(); + var requestTimer = window.setTimeout(function () { + recoveryController.abort(); + }, delay); window - .fetch(documentUrl.href, { cache: "no-store", credentials: "same-origin" }) + .fetch(documentUrl.href, { + cache: "no-store", + credentials: "same-origin", + signal: recoveryController.signal, + }) .then(function (response) { if (!response.ok) throw new Error("gateway unavailable"); window.location.replace(documentUrl.href); }) .catch(function () { finishRecoveryAttempt(); + }) + .finally(function () { + window.clearTimeout(requestTimer); }); } diff --git a/ui/src/app/mount-fallback.test.ts b/ui/src/app/mount-fallback.test.ts index 8c8ad13c6268..8a238f068796 100644 --- a/ui/src/app/mount-fallback.test.ts +++ b/ui/src/app/mount-fallback.test.ts @@ -125,10 +125,39 @@ describe("Control UI mount fallback", () => { await vi.waitFor(() => expect(fetch).toHaveBeenCalled()); - expect(fetch).toHaveBeenNthCalledWith(1, expect.stringContaining("openclaw_mount_recovery="), { - cache: "no-store", - credentials: "same-origin", + expect(fetch).toHaveBeenNthCalledWith( + 1, + expect.stringContaining("openclaw_mount_recovery="), + expect.objectContaining({ + cache: "no-store", + credentials: "same-origin", + signal: expect.any(frameWindow.AbortSignal), + }), + ); + }); + + it("times out stalled recovery probes so automatic retries can continue", async () => { + const frameWindow = createIsolatedWindow(); + const signals: AbortSignal[] = []; + const fetch = vi.fn((_url: string, init?: RequestInit) => { + const signal = init?.signal; + if (!(signal instanceof frameWindow.AbortSignal)) { + throw new Error("Expected recovery probe to include an abort signal"); + } + signals.push(signal); + return new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => reject(new Error("request aborted")), { + once: true, + }); + }); }); + Object.defineProperty(frameWindow, "fetch", { configurable: true, value: fetch }); + installFallbackShell(frameWindow, await readIndexHtmlWithDelay(1)); + + await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(6)); + + expect(signals).toHaveLength(6); + await vi.waitFor(() => expect(signals.every((signal) => signal.aborted)).toBe(true)); }); it("bounds automatic recovery attempts while the gateway is unavailable", async () => {