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 () => {