mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(browser): align bare ws cdp readiness
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Browser/CDP: make readiness diagnostics use the same discovery-first fallback as reachability for bare `ws://` Browserless and Browserbase CDP URLs. Fixes #69532.
|
||||
- Memory-host SDK: use trusted env-proxy mode for remote embedding and batch HTTP calls only when Undici will proxy that target, preserving SSRF DNS pinning for `ALL_PROXY`-only and `NO_PROXY` bypass cases. Fixes #52162. (#71506) Thanks @DhtIsCoding.
|
||||
- Gateway/dashboard: render Control UI and WebSocket links with `https://`/`wss://` when `gateway.tls.enabled=true`, including `openclaw gateway status`. Fixes #71494. (#71499) Thanks @deepkilo.
|
||||
- Agents/OpenAI-compatible: default proxy/local completions tool requests to `tool_choice: "auto"` when tools are present, so providers enter native tool-calling mode instead of replying with plain-text tool directives. (#71472) Thanks @Speed-maker.
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
appendCdpPath,
|
||||
assertCdpEndpointAllowed,
|
||||
fetchCdpChecked,
|
||||
isDirectCdpWebSocketEndpoint,
|
||||
isWebSocketUrl,
|
||||
normalizeCdpHttpBaseForJsonEndpoints,
|
||||
openCdpWebSocket,
|
||||
redactCdpUrl,
|
||||
} from "./cdp.helpers.js";
|
||||
@@ -266,7 +268,7 @@ export async function diagnoseChromeCdp(
|
||||
});
|
||||
}
|
||||
|
||||
if (isWebSocketUrl(cdpUrl)) {
|
||||
if (isDirectCdpWebSocketEndpoint(cdpUrl)) {
|
||||
const health = await diagnoseCdpHealthCommand(cdpUrl, handshakeTimeoutMs);
|
||||
if (!health.ok) {
|
||||
return failureDiagnostic({
|
||||
@@ -285,10 +287,31 @@ export async function diagnoseChromeCdp(
|
||||
};
|
||||
}
|
||||
|
||||
const discoveryUrl = isWebSocketUrl(cdpUrl)
|
||||
? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)
|
||||
: cdpUrl;
|
||||
let version: ChromeVersion;
|
||||
try {
|
||||
version = await readChromeVersion(cdpUrl, timeoutMs, ssrfPolicy);
|
||||
version = await readChromeVersion(discoveryUrl, timeoutMs, ssrfPolicy);
|
||||
} catch (err) {
|
||||
if (isWebSocketUrl(cdpUrl)) {
|
||||
const health = await diagnoseCdpHealthCommand(cdpUrl, handshakeTimeoutMs);
|
||||
if (!health.ok) {
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
wsUrl: cdpUrl,
|
||||
code: health.code,
|
||||
message: health.message,
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
cdpUrl,
|
||||
wsUrl: cdpUrl,
|
||||
elapsedMs: elapsedSince(startedAt),
|
||||
};
|
||||
}
|
||||
const classified = classifyChromeVersionError(err);
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
@@ -300,6 +323,26 @@ export async function diagnoseChromeCdp(
|
||||
|
||||
const wsUrlRaw = normalizeOptionalString(version.webSocketDebuggerUrl) ?? "";
|
||||
if (!wsUrlRaw) {
|
||||
if (isWebSocketUrl(cdpUrl)) {
|
||||
const health = await diagnoseCdpHealthCommand(cdpUrl, handshakeTimeoutMs);
|
||||
if (!health.ok) {
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
wsUrl: cdpUrl,
|
||||
code: health.code,
|
||||
message: health.message,
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
cdpUrl,
|
||||
wsUrl: cdpUrl,
|
||||
browser: version.Browser,
|
||||
userAgent: version["User-Agent"],
|
||||
elapsedMs: elapsedSince(startedAt),
|
||||
};
|
||||
}
|
||||
return failureDiagnostic({
|
||||
cdpUrl,
|
||||
code: "missing_websocket_debugger_url",
|
||||
@@ -307,7 +350,7 @@ export async function diagnoseChromeCdp(
|
||||
startedAt,
|
||||
});
|
||||
}
|
||||
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, cdpUrl);
|
||||
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, discoveryUrl);
|
||||
try {
|
||||
await assertCdpEndpointAllowed(wsUrl, ssrfPolicy);
|
||||
} catch (err) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import {
|
||||
parseBrowserMajorVersion,
|
||||
resolveGoogleChromeExecutableForPlatform,
|
||||
@@ -56,7 +57,7 @@ async function withMockChromeCdpServer(params: {
|
||||
run: (baseUrl: string) => Promise<void>;
|
||||
}) {
|
||||
const server = createServer((req, res) => {
|
||||
if (req.url === "/json/version") {
|
||||
if (req.url?.startsWith("/json/version")) {
|
||||
const addr = server.address() as AddressInfo;
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(
|
||||
@@ -71,7 +72,7 @@ async function withMockChromeCdpServer(params: {
|
||||
});
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
if (req.url !== params.wsPath) {
|
||||
if (!req.url?.startsWith(params.wsPath)) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
@@ -630,6 +631,37 @@ describe("browser chrome helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses HTTP discovery before readiness checks for a bare ws:// CDP URL", async () => {
|
||||
await withMockChromeCdpServer({
|
||||
wsPath: "/devtools/browser/READY",
|
||||
onConnection: (wss) => {
|
||||
wss.on("connection", (ws) => {
|
||||
ws.on("message", (raw) => {
|
||||
const message = JSON.parse(rawDataToString(raw)) as { id?: number; method?: string };
|
||||
if (message.method === "Browser.getVersion" && message.id === 1) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
id: 1,
|
||||
result: { product: "Chrome/Mock" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
run: async (baseUrl) => {
|
||||
const url = new URL(baseUrl);
|
||||
const wsOnlyBase = `ws://${url.host}?token=abc`;
|
||||
await expect(isChromeCdpReady(wsOnlyBase, 300, 400)).resolves.toBe(true);
|
||||
const diagnostic = await diagnoseChromeCdp(wsOnlyBase, 300, 400);
|
||||
expect(diagnostic).toMatchObject({
|
||||
ok: true,
|
||||
wsUrl: `ws://${url.host}/devtools/browser/READY?token=abc`,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports unreachable when a bare ws:// CDP URL points at a server with no /json/version and refuses WS", async () => {
|
||||
// Negative counterpart to the #68027 happy path — a bare ws URL
|
||||
// pointed at a port that neither serves /json/version nor accepts
|
||||
@@ -664,6 +696,36 @@ describe("browser chrome helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to a direct WS readiness check when /json/version has no debugger URL", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({}),
|
||||
} as unknown as Response),
|
||||
);
|
||||
const wss = new WebSocketServer({ port: 0, host: "127.0.0.1" });
|
||||
wss.on("connection", (ws) => {
|
||||
ws.on("message", (raw) => {
|
||||
const message = JSON.parse(rawDataToString(raw)) as { id?: number; method?: string };
|
||||
if (message.method === "Browser.getVersion" && message.id === 1) {
|
||||
ws.send(JSON.stringify({ id: 1, result: { product: "Browserless/Mock" } }));
|
||||
}
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve) => wss.once("listening", () => resolve()));
|
||||
const port = (wss.address() as AddressInfo).port;
|
||||
try {
|
||||
await expect(isChromeCdpReady(`ws://127.0.0.1:${port}`, 500, 500)).resolves.toBe(true);
|
||||
await expect(diagnoseChromeCdp(`ws://127.0.0.1:${port}`, 500, 500)).resolves.toMatchObject({
|
||||
ok: true,
|
||||
wsUrl: `ws://127.0.0.1:${port}`,
|
||||
});
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => wss.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
it("returns the original ws:// URL from getChromeWebSocketUrl when /json/version provides no debugger URL", async () => {
|
||||
// Covers the getChromeWebSocketUrl WS-fallback: discovery succeeds but
|
||||
// webSocketDebuggerUrl is absent — the original URL is returned as-is.
|
||||
|
||||
Reference in New Issue
Block a user