mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(browser): diagnose empty WSL2 Chrome replies (#100590)
* fix(browser): diagnose Windows CDP listener mismatch Base the repair on #93481 while matching Chromium's IPv4-first, IPv6-fallback listener contract and keeping CDP exposure loopback-scoped. Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com> * style(docs): format browser troubleshooting table * docs: refresh browser troubleshooting map * fix(browser): diagnose reset CDP replies * docs: refresh browser troubleshooting map --------- Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com>
This commit is contained in:
committed by
GitHub
parent
5713f1bec1
commit
2830b3ef38
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
|
||||
- **TUI startup status:** show `starting up` during post-connect initialization without overwriting active-run or reconnect state. (#93999) Thanks @ml12580.
|
||||
- **Control UI restart recovery:** recover stale bundle pages through a bounded whole-document refresh after Gateway updates or restarts. (#99111) Thanks @ZengWen-DT.
|
||||
- **TUI active Gateway ports:** follow the verified active local Gateway port when no explicit URL, port, or remote target is configured. (#73338, #42461) Thanks @haishmg and @vincentkoc.
|
||||
- **Windows remote browser diagnostics:** distinguish WSL2 portproxy self-loops from Chromium's IPv6 fallback using host listener checks, document source-accurate `v4tov6` recovery, require a non-default profile for Chrome 136+ command-line debugging, and warn against broad CDP exposure. (#54669) Thanks @Owlock and @ZengWen-DT.
|
||||
- **Apple chat run recovery:** restore active responses from canonical Gateway history after reconnects, foreground resumes, and event gaps, while preserving gateway user-turn identity across Codex and Copilot transcript mirrors to prevent duplicate rows. (#100277)
|
||||
- **Claude CLI streamed replies:** preserve assistant text already received from Claude CLI when its terminal result envelope is empty, preventing false empty-response failover after a complete streamed answer. (#90450) Thanks @totobusnello.
|
||||
- **Phone identity normalization:** canonicalize stray plus signs, preserve non-phone iMessage handles, and reject digit-free Signal identities across shared channel routing. (#100467) Thanks @morluto.
|
||||
|
||||
@@ -8967,6 +8967,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- H2: Critical rule for the Control UI
|
||||
- H2: Validate in layers
|
||||
- H3: Layer 1: verify Chrome is serving CDP on Windows
|
||||
- H4: Diagnose IPv4 and IPv6 before changing portproxy
|
||||
- H3: Layer 2: verify WSL2 can reach that Windows endpoint
|
||||
- H3: Layer 3: configure the correct browser profile
|
||||
- H3: Layer 4: verify the Control UI layer separately
|
||||
@@ -9497,6 +9498,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- H2: Storage
|
||||
- H2: Limits
|
||||
- H2: Troubleshooting
|
||||
- H3: Tool-policy diagnostic
|
||||
- H2: Related
|
||||
|
||||
## tools/skills-config.md
|
||||
|
||||
@@ -63,17 +63,58 @@ different error visible from a layer further down.
|
||||
### Layer 1: verify Chrome is serving CDP on Windows
|
||||
|
||||
```powershell
|
||||
chrome.exe --remote-debugging-port=9222
|
||||
chrome.exe --remote-debugging-port=9222 --user-data-dir="$env:LOCALAPPDATA\OpenClaw\ChromeCDP"
|
||||
```
|
||||
|
||||
Chrome 136 and later ignore remote-debugging command-line switches for the
|
||||
default Chrome data directory. Use a separate, non-default data directory as
|
||||
shown above. See Chrome's
|
||||
[remote-debugging security change](https://developer.chrome.com/blog/remote-debugging-port).
|
||||
This does not make the normal signed-in Chrome profile remotely controllable.
|
||||
|
||||
From Windows, verify Chrome itself first:
|
||||
|
||||
```powershell
|
||||
curl http://127.0.0.1:9222/json/version
|
||||
curl http://127.0.0.1:9222/json/list
|
||||
curl.exe http://127.0.0.1:9222/json/version
|
||||
curl.exe http://127.0.0.1:9222/json/list
|
||||
```
|
||||
|
||||
If this fails on Windows, OpenClaw is not the problem yet.
|
||||
If this fails, diagnose the Windows listeners below. OpenClaw is not the
|
||||
problem yet.
|
||||
|
||||
#### Diagnose IPv4 and IPv6 before changing portproxy
|
||||
|
||||
Chromium tries to bind remote debugging to `127.0.0.1` first and falls back to
|
||||
`[::1]` only if the IPv4 bind fails. A persistent `v4tov4` rule listening on
|
||||
`127.0.0.1:9222` can occupy that endpoint before Chrome starts. Chrome then
|
||||
falls back to `[::1]:9222`, while the old rule forwards IPv4 traffic back to
|
||||
its own listener and returns an empty reply.
|
||||
|
||||
Check the actual listeners and proxy rules from Windows instead of inferring
|
||||
them from the Chrome version:
|
||||
|
||||
```powershell
|
||||
netstat -ano | findstr :9222
|
||||
netsh interface portproxy show all
|
||||
curl.exe http://127.0.0.1:9222/json/version
|
||||
curl.exe http://[::1]:9222/json/version
|
||||
```
|
||||
|
||||
Use `tasklist /fi "PID eq <PID>"` for each PID from `netstat`.
|
||||
|
||||
- If `chrome.exe` answers on `127.0.0.1`, remove any portproxy rule that also
|
||||
listens on `127.0.0.1:9222`. Forward only the WSL2-reachable Windows adapter
|
||||
address to `127.0.0.1`.
|
||||
- If `chrome.exe` answers only on `[::1]`, point the WSL2-reachable listener at
|
||||
`::1` with `v4tov6` instead of forwarding to an unused IPv4 address:
|
||||
|
||||
```powershell
|
||||
netsh interface portproxy add v4tov6 listenaddress=WINDOWS_HOST_OR_IP listenport=9222 connectaddress=::1 connectport=9222
|
||||
```
|
||||
|
||||
Bind the listener to the adapter address WSL2 needs. Do not expose the CDP
|
||||
port on `0.0.0.0`, a LAN address, or a tailnet address: CDP grants control of
|
||||
the browser session.
|
||||
|
||||
### Layer 2: verify WSL2 can reach that Windows endpoint
|
||||
|
||||
@@ -158,6 +199,7 @@ Good result:
|
||||
| `token_missing` | auth configuration problem |
|
||||
| `pairing required` | device approval problem |
|
||||
| `Remote CDP for profile "remote" is not reachable` | WSL2 cannot reach the configured `cdpUrl` |
|
||||
| empty CDP reply / `other side closed` through a portproxy | Windows listener mismatch or a self-loop; inspect both loopback families and `netsh interface portproxy show all` |
|
||||
| `Browser attachOnly is enabled and CDP websocket for profile "remote" is not reachable` | the HTTP endpoint answered, but the DevTools WebSocket could not be opened |
|
||||
| stale viewport / dark-mode / locale / offline overrides after a remote session | run `openclaw browser --browser-profile remote stop` to close the session and release the cached Playwright/CDP connection without restarting the Gateway or the external browser |
|
||||
| timeout around `remoteCdpTimeoutMs` (default 1500ms) | usually still CDP reachability, or a slow/unreachable remote endpoint |
|
||||
@@ -166,7 +208,8 @@ Good result:
|
||||
|
||||
## Fast triage checklist
|
||||
|
||||
1. Windows: does `curl http://127.0.0.1:9222/json/version` work?
|
||||
1. Windows: which of `127.0.0.1` or `[::1]` answers on `/json/version`, and
|
||||
does that listener belong to `chrome.exe`?
|
||||
2. WSL2: does `curl http://WINDOWS_HOST_OR_IP:9222/json/version` work?
|
||||
3. OpenClaw config: does `browser.profiles.<name>.cdpUrl` use that exact
|
||||
WSL2-reachable address?
|
||||
|
||||
@@ -272,13 +272,25 @@ export function formatChromeCdpDiagnostic(diagnostic: ChromeCdpDiagnostic): stri
|
||||
const websocket = redactedWsUrl ? `; websocket=${redactedWsUrl}` : "";
|
||||
const wslPortproxyHint =
|
||||
diagnostic.code === "http_unreachable" && isLikelyEmptyHttpReply(diagnostic.message)
|
||||
? " In WSL2-to-Windows Chrome setups, this can be a stale netsh portproxy self-loop where svchost/iphlpsvc owns the CDP port instead of chrome.exe; verify with tasklist /svc and curl /json/version, then remove any 127.0.0.1:9222 -> 127.0.0.1:9222 portproxy rule."
|
||||
? WSL_EMPTY_REPLY_PORTPROXY_HINT
|
||||
: "";
|
||||
return `CDP diagnostic: ${diagnostic.code} after ${diagnostic.elapsedMs}ms; cdp=${redactedCdpUrl}${websocket}; ${diagnostic.message}.${wslPortproxyHint}`;
|
||||
}
|
||||
|
||||
// The WSL-side error cannot identify which Windows loopback Chrome owns.
|
||||
// Send operators to the host listeners before they change the proxy family.
|
||||
const WSL_EMPTY_REPLY_PORTPROXY_HINT =
|
||||
" In WSL2-to-Windows Chrome setups, an empty CDP reply can mean netsh is forwarding to the" +
|
||||
" wrong loopback address. On Windows, inspect `netstat -ano | findstr :9222` and" +
|
||||
" `netsh interface portproxy show all`, then curl both 127.0.0.1 and [::1]. Chromium prefers" +
|
||||
" 127.0.0.1 and falls back to [::1] only when the IPv4 bind fails. If svchost/iphlpsvc owns" +
|
||||
" 127.0.0.1:9222, remove the 127.0.0.1:9222 -> 127.0.0.1:9222 self-loop; if chrome.exe" +
|
||||
" listens only on [::1], use v4tov6 with connectaddress=::1 for the WSL2-reachable listener.";
|
||||
|
||||
function isLikelyEmptyHttpReply(message: string): boolean {
|
||||
return /empty reply|other side closed|socket closed|terminated before response/i.test(message);
|
||||
return /empty reply|other side closed|socket closed|connection reset|econnreset|terminated before response/i.test(
|
||||
message,
|
||||
);
|
||||
}
|
||||
|
||||
async function diagnoseCdpWebSocketEndpoint(params: {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import fs from "node:fs";
|
||||
import fsp from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import { createServer as createTcpServer } from "node:net";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -671,17 +672,48 @@ describe("browser chrome helpers", () => {
|
||||
expect(formatted).not.toContain("supersecret123");
|
||||
});
|
||||
|
||||
it("adds a WSL2 portproxy hint for empty HTTP CDP replies", () => {
|
||||
const formatted = formatChromeCdpDiagnostic({
|
||||
ok: false,
|
||||
code: "http_unreachable",
|
||||
cdpUrl: "http://172.30.144.1:9222",
|
||||
message: "fetch failed: other side closed",
|
||||
elapsedMs: 12,
|
||||
});
|
||||
it.each(["fetch failed: other side closed", "fetch failed: read ECONNRESET"])(
|
||||
"adds a WSL2 portproxy hint for empty HTTP CDP replies: %s",
|
||||
(message) => {
|
||||
const formatted = formatChromeCdpDiagnostic({
|
||||
ok: false,
|
||||
code: "http_unreachable",
|
||||
cdpUrl: "http://172.30.144.1:9222",
|
||||
message,
|
||||
elapsedMs: 12,
|
||||
});
|
||||
|
||||
expect(formatted).toContain("svchost/iphlpsvc owns the CDP port");
|
||||
expect(formatted).toContain("127.0.0.1:9222 -> 127.0.0.1:9222");
|
||||
expect(formatted).toContain("netsh interface portproxy show all");
|
||||
expect(formatted).toContain("svchost/iphlpsvc owns");
|
||||
expect(formatted).toContain("127.0.0.1:9222 -> 127.0.0.1:9222");
|
||||
expect(formatted).toContain("falls back to [::1] only when the IPv4 bind fails");
|
||||
expect(formatted).toContain("v4tov6");
|
||||
},
|
||||
);
|
||||
|
||||
it("surfaces Windows listener checks from a real empty-reply CDP probe", async () => {
|
||||
// A broken portproxy accepts the WSL-side socket and closes it without an
|
||||
// HTTP body. The host checks must survive the full probe/format path.
|
||||
const portproxy = createTcpServer((socket) => socket.destroy());
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
portproxy.listen(0, "127.0.0.1", () => resolve());
|
||||
portproxy.once("error", reject);
|
||||
});
|
||||
try {
|
||||
const addr = portproxy.address() as AddressInfo;
|
||||
const diagnostic = expectFailedChromeCdpDiagnostic(
|
||||
await diagnoseChromeCdp(`http://127.0.0.1:${addr.port}`, 500, 50),
|
||||
);
|
||||
expect(diagnostic.code).toBe("http_unreachable");
|
||||
const formatted = formatChromeCdpDiagnostic(diagnostic);
|
||||
expect(formatted).toContain("netstat -ano");
|
||||
expect(formatted).toContain("v4tov6");
|
||||
expect(formatted).not.toContain("Chrome 136");
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => {
|
||||
portproxy.close(() => resolve());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("probes direct ws:// CDP URLs (with /devtools/ path) via handshake instead of HTTP", async () => {
|
||||
|
||||
Reference in New Issue
Block a user