Skip to content

[Bug]: ensurePortAvailable() pre-flight misses IPv4-only loopback occupant → Chrome CDP bind fails with misleading "HTTP 401" #94379

Description

@wangwllu

Bug type

Behavior bug (misleading error / incorrect pre-flight result)

Summary

ensurePortAvailable() (the pre-flight run before launching the managed Chrome in launchOpenClawChrome) uses a bare net.listen({ port }) with no host. On a dual-stack machine a host-less listener binds to the IPv6 wildcard ::, so an IPv4-only occupant of 127.0.0.1:<cdpPort> is not detected. The pre-flight reports the port as free, OpenClaw proceeds to spawn Chrome with --remote-debugging-port=<cdpPort>, and Chrome — which binds the CDP port on 127.0.0.1 — then fails with bind() failed: Address already in use (48). The CDP HTTP endpoint never comes up, and the surface error the user sees is a confusing Failed to start Chrome CDP ... HTTP 401, not a clear "port in use".

The address family the pre-flight probes (::) does not match the address family Chrome actually binds (127.0.0.1), so the guard that exists specifically to fail fast is silently bypassed.

Steps to reproduce

  1. Occupy the managed CDP port on IPv4 loopback only (any process bound to 127.0.0.1:18800; in my case a second OpenClaw-family gateway grabbed it first).
  2. Start the OpenClaw browser on the default profile (openclaw browser start / any browser tool call), whose cdpPort defaults to 18800.
  3. Observe launch fails.

Minimal proof that the pre-flight probe disagrees with Chrome's bind (run while 127.0.0.1:18800 is occupied IPv4-only):

const net = require("node:net");
const tryListen = (opts) => new Promise((res, rej) => {
  const s = net.createServer().once("error", rej).once("listening", () => s.close(() => res("free"))).listen(opts);
});
(async () => {
  for (const opts of [{port:18800}, {port:18800,host:"127.0.0.1"}, {port:18800,host:"::"}, {port:18800,host:"0.0.0.0"}]) {
    try { console.log(JSON.stringify(opts), "=>", await tryListen(opts)); }
    catch (e) { console.log(JSON.stringify(opts), "=>", e.code); }
  }
})();

Output:

{"port":18800} => free                       <- what ensurePortAvailable() does → false negative
{"port":18800,"host":"127.0.0.1"} => EADDRINUSE   <- what Chrome actually hits
{"port":18800,"host":"::"} => free
{"port":18800,"host":"0.0.0.0"} => free

Expected behavior

ensurePortAvailable() should detect an IPv4-only occupant of the loopback CDP port and fail fast with the clear Port <n> is already in use. error (it already has nice owner-diagnostics via describePortOwner/handlePortError). The pre-flight should probe the same host family Chrome binds (127.0.0.1), or probe all loopback families.

Actual behavior

Pre-flight passes (false negative) → Chrome bind() fails → user sees Failed to start Chrome CDP on port <n> ... HTTP 401 + bind() failed: Address already in use. The 401 strongly misdirects toward auth/proxy debugging; the real cause is a plain port collision the guard was meant to catch.

Root cause

  • ensurePortAvailable(port)tryListenOnPort({ port }) (no host) in src/infra/ports-probe.ts. A host-less net.listen binds the IPv6 wildcard on dual-stack hosts and does not observe an IPv4-only listener.
  • Contrast: the sibling checkPortInUse(port) in the same area does probe all four families ["127.0.0.1","0.0.0.0","::1","::"] and would have caught this. ensurePortAvailable (the one on the Chrome-launch path) does not reuse it.
  • Chrome binds the debugging port on 127.0.0.1 (loopback), so the family mismatch is the whole bug.

Suggested fix

Make ensurePortAvailable consistent with Chrome's bind: either probe 127.0.0.1 explicitly, or delegate to the existing multi-family checkPortInUse/inspectPortUsage and throw PortInUseError when any loopback family is busy. Low blast radius — the multi-family probe already exists in the same module.

OpenClaw version

2026.6.8 (844f405)

Operating system

macOS 26.4 (arm64)

Node.js

v26.0.0 (bundled)

Chrome

149.0.7827.116

Install method

npm global

Impact and severity

  • Severity: Medium. Not a crash, but the surfaced HTTP 401 actively misdirects diagnosis (I initially chased auth/SSRF/proxy before finding the IPv4/IPv6 family mismatch).
  • Frequency: 100% whenever the loopback CDP port has an IPv4-only occupant.
  • The guard meant to produce a clear actionable error is silently bypassed.

Additional notes

The IPv4 occupant in my environment was a second gateway sharing the default 18800 range; I worked around it by pinning the profile's cdpPort to a free port. The dual-instance setup is incidental — the reportable defect is the pre-flight's address-family mismatch and the resulting misleading error, which reproduces for any IPv4-only occupant of the loopback CDP port.

Metadata

Metadata

Assignees

Labels

P2Normal backlog priority with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions