fix(browser): tolerate brief extension relay disconnects on attached tabs

Keep extension relay tab metadata available across short extension worker drops and allow CDP clients to connect while waiting for reconnect. This prevents false "no tab connected" failures in environments where the extension worker disconnects transiently (e.g. WSLg/MV3).
This commit is contained in:
SidQin-cyber
2026-03-01 10:28:48 +08:00
committed by Peter Steinberger
parent 0eebae44f6
commit f77f3fb839
2 changed files with 54 additions and 7 deletions

View File

@@ -392,6 +392,53 @@ describe("chrome extension relay server", () => {
ext2.close();
});
it("keeps /json/version websocket endpoint during short extension disconnects", async () => {
const { port, ext } = await startRelayWithExtension();
ext.send(
JSON.stringify({
method: "forwardCDPEvent",
params: {
method: "Target.attachedToTarget",
params: {
sessionId: "cb-tab-disconnect",
targetInfo: {
targetId: "t-disconnect",
type: "page",
title: "Disconnect test",
url: "https://example.com",
},
waitingForDebugger: false,
},
},
}),
);
await waitForListMatch(
async () =>
(await fetch(`${cdpUrl}/json/list`, {
headers: relayAuthHeaders(cdpUrl),
}).then((r) => r.json())) as Array<{ id?: string }>,
(list) => list.some((entry) => entry.id === "t-disconnect"),
);
const extClosed = waitForClose(ext, 2_000);
ext.close();
await extClosed;
const version = (await fetch(`${cdpUrl}/json/version`, {
headers: relayAuthHeaders(cdpUrl),
}).then((r) => r.json())) as {
webSocketDebuggerUrl?: string;
};
expect(String(version.webSocketDebuggerUrl ?? "")).toContain("/cdp");
const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`, {
headers: relayAuthHeaders(`ws://127.0.0.1:${port}/cdp`),
});
await waitForOpen(cdp);
cdp.close();
});
it("waits briefly for extension reconnect before failing CDP commands", async () => {
const { port, ext: ext1 } = await startRelayWithExtension();
const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`, {

View File

@@ -82,7 +82,7 @@ type ConnectedTarget = {
};
const RELAY_AUTH_HEADER = "x-openclaw-relay-token";
const DEFAULT_EXTENSION_RECONNECT_GRACE_MS = 5_000;
const DEFAULT_EXTENSION_RECONNECT_GRACE_MS = 20_000;
const DEFAULT_EXTENSION_COMMAND_RECONNECT_WAIT_MS = 3_000;
function headerValue(value: string | string[] | undefined): string | undefined {
@@ -256,6 +256,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
const cdpClients = new Set<WebSocket>();
const connectedTargets = new Map<string, ConnectedTarget>();
const extensionConnected = () => extensionWs?.readyState === WebSocket.OPEN;
const hasConnectedTargets = () => connectedTargets.size > 0;
let extensionDisconnectCleanupTimer: NodeJS.Timeout | null = null;
const extensionReconnectWaiters = new Set<(connected: boolean) => void>();
@@ -534,8 +535,9 @@ export async function ensureChromeExtensionRelayServer(opts: {
Browser: "OpenClaw/extension-relay",
"Protocol-Version": "1.3",
};
// Only advertise the WS URL if a real extension is connected.
if (extensionConnected()) {
// Keep reporting CDP WS while attached targets are cached, so callers can
// reconnect through brief MV3 worker disconnects.
if (extensionConnected() || hasConnectedTargets()) {
payload.webSocketDebuggerUrl = cdpWsUrl;
}
res.writeHead(200, { "Content-Type": "application/json" });
@@ -658,10 +660,8 @@ export async function ensureChromeExtensionRelayServer(opts: {
rejectUpgrade(socket, 401, "Unauthorized");
return;
}
if (!extensionConnected()) {
rejectUpgrade(socket, 503, "Extension not connected");
return;
}
// Allow CDP clients to connect even during brief extension worker drops.
// Individual commands already wait briefly for extension reconnect.
wssCdp.handleUpgrade(req, socket, head, (ws) => {
wssCdp.emit("connection", ws, req);
});