mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: Canvas WebSocket creation in browser context silently swallows... (#82505)
* fix(canvas): report live reload websocket errors Surface WebSocket initialization failures from the injected canvas live reload client through console diagnostics, DOM attributes, and a browser event instead of silently swallowing them. * refactor(canvas): keep reload diagnostics console-only * fix(canvas): distinguish page teardown from socket failure * style(canvas): brace pagehide listener --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -27,6 +27,16 @@ export function injectCanvasLiveReload(html: string): string {
|
||||
// - iOS: window.webkit.messageHandlers.openclawCanvasA2UIAction.postMessage(...)
|
||||
// - Android: window.openclawCanvasA2UIAction.postMessage(...)
|
||||
const handlerNames = ["openclawCanvasA2UIAction"];
|
||||
let liveReloadErrorReported = false;
|
||||
let pageUnloading = false;
|
||||
globalThis.addEventListener?.("pagehide", () => { pageUnloading = true; }, { once: true });
|
||||
function reportCanvasLiveReloadError(err) {
|
||||
if (liveReloadErrorReported) return;
|
||||
liveReloadErrorReported = true;
|
||||
try {
|
||||
console.error("OpenClaw canvas live reload unavailable:", err);
|
||||
} catch {}
|
||||
}
|
||||
function postToNode(payload) {
|
||||
try {
|
||||
const raw = typeof payload === "string" ? payload : JSON.stringify(payload);
|
||||
@@ -67,7 +77,17 @@ export function injectCanvasLiveReload(html: string): string {
|
||||
ws.onmessage = (ev) => {
|
||||
if (String(ev.data || "") === "reload") location.reload();
|
||||
};
|
||||
} catch {}
|
||||
ws.onerror = (ev) => {
|
||||
reportCanvasLiveReloadError(ev);
|
||||
};
|
||||
ws.onclose = (ev) => {
|
||||
if (ev.code !== 1000 && !(ev.code === 1001 && pageUnloading)) {
|
||||
reportCanvasLiveReloadError(ev);
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
reportCanvasLiveReloadError(err);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
`.trim();
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { IncomingMessage } from "node:http";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { Duplex } from "node:stream";
|
||||
import vm from "node:vm";
|
||||
import { defaultRuntime } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
@@ -117,6 +118,51 @@ async function captureA2uiFixtureResponse(
|
||||
return await captureHttpResponse(createA2uiHttpRequestHandler({ rootDir }), url, method);
|
||||
}
|
||||
|
||||
function extractInjectedScript(html: string): string {
|
||||
const match = html.match(/<script>([\s\S]+)<\/script>/);
|
||||
if (!match?.[1]) {
|
||||
throw new Error("expected injected canvas live reload script");
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
type MockLiveReloadSocket = {
|
||||
onclose?: (event: { code: number; reason: string }) => void;
|
||||
onerror?: (event: Error) => void;
|
||||
onmessage?: (event: { data?: string }) => void;
|
||||
};
|
||||
|
||||
function runInjectedScript(WebSocket: (this: MockLiveReloadSocket) => void) {
|
||||
const consoleError = vi.fn();
|
||||
let pagehide: (() => void) | undefined;
|
||||
const runtime: Record<string, unknown> = {
|
||||
URLSearchParams,
|
||||
WebSocket,
|
||||
addEventListener: (event: string, listener: () => void) => {
|
||||
if (event === "pagehide") {
|
||||
pagehide = listener;
|
||||
}
|
||||
},
|
||||
console: { error: consoleError },
|
||||
encodeURIComponent,
|
||||
location: {
|
||||
host: "control.example",
|
||||
protocol: "https:",
|
||||
reload: vi.fn(),
|
||||
search: "",
|
||||
},
|
||||
};
|
||||
runtime.window = runtime;
|
||||
runtime.self = runtime;
|
||||
runtime.globalThis = runtime;
|
||||
vm.createContext(runtime);
|
||||
vm.runInContext(
|
||||
extractInjectedScript(injectCanvasLiveReload("<html><body>Hello</body></html>")),
|
||||
runtime,
|
||||
);
|
||||
return { consoleError, pagehide: () => pagehide?.() };
|
||||
}
|
||||
|
||||
describe("canvas host", () => {
|
||||
const quietRuntime = {
|
||||
...defaultRuntime,
|
||||
@@ -194,6 +240,56 @@ describe("canvas host", () => {
|
||||
expect(out).toContain("openclawSendUserAction");
|
||||
});
|
||||
|
||||
it("reports websocket initialization errors instead of swallowing them silently", () => {
|
||||
function ThrowingWebSocket(): never {
|
||||
throw new TypeError("constructor failed");
|
||||
}
|
||||
const { consoleError } = runInjectedScript(ThrowingWebSocket);
|
||||
|
||||
expect(consoleError).toHaveBeenCalledTimes(1);
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
"OpenClaw canvas live reload unavailable:",
|
||||
expect.objectContaining({ message: "constructor failed" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports asynchronous websocket connection errors once", () => {
|
||||
const sockets: MockLiveReloadSocket[] = [];
|
||||
function CapturingWebSocket(this: MockLiveReloadSocket): void {
|
||||
sockets.push(this);
|
||||
}
|
||||
const { consoleError } = runInjectedScript(CapturingWebSocket);
|
||||
|
||||
expect(sockets).toHaveLength(1);
|
||||
sockets[0]?.onerror?.(new Error("connect failed"));
|
||||
sockets[0]?.onclose?.({ code: 1006, reason: "abnormal closure" });
|
||||
|
||||
expect(consoleError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not report a normal close after connecting", () => {
|
||||
const sockets: MockLiveReloadSocket[] = [];
|
||||
const { consoleError, pagehide } = runInjectedScript(function (this: MockLiveReloadSocket) {
|
||||
sockets.push(this);
|
||||
});
|
||||
|
||||
pagehide();
|
||||
sockets[0]?.onclose?.({ code: 1001, reason: "page closed" });
|
||||
|
||||
expect(consoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports an abnormal close without a preceding error event", () => {
|
||||
const sockets: MockLiveReloadSocket[] = [];
|
||||
const { consoleError } = runInjectedScript(function (this: MockLiveReloadSocket) {
|
||||
sockets.push(this);
|
||||
});
|
||||
|
||||
sockets[0]?.onclose?.({ code: 1011, reason: "server error" });
|
||||
|
||||
expect(consoleError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("creates a default index.html when missing", async () => {
|
||||
const dir = await createCaseDir();
|
||||
const handler = await createTestCanvasHostHandler(dir);
|
||||
|
||||
Reference in New Issue
Block a user