openclaw - ✅(Solved) Fix status/doctor reports false Gateway port conflict for healthy LAN-bound gateway [1 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#77939Fetched 2026-05-06 06:19:03
View on GitHub
Comments
2
Participants
3
Timeline
3
Reactions
3
Author
Timeline (top)
commented ×2cross-referenced ×1

openclaw status --all / doctor currently treats a healthy managed OpenClaw Gateway listener as a port conflict when the gateway is intentionally bound on a non-loopback/LAN address for an internal reverse-proxy deployment.

This is the same false-positive class as #53398, but #53398 only covered the dual-stack loopback case (127.0.0.1 + ::1). A valid gateway.bind=lan / wildcard bind setup can still render misleading output such as:

  • Port 18789 is already in use.
  • Gateway already running locally. Stop it (...) or use a different port.

That wording is wrong when the only listener on the configured port is the expected managed OpenClaw Gateway and gateway health/liveness succeeds.

Error Message

That configuration can be unusual, but it is valid. It may deserve separate exposure/security/audit guidance, but it should not appear as a port-conflict error. Still warn: emitCheck(Port ${params.port}, portOk ? "ok" : "warn");

  • two different OpenClaw Gateway PIDs still warn

Root Cause

Some deployments intentionally do not use Tailscale and instead expose OpenClaw only through a controlled internal / allowlisted reverse-proxy path. In that setup the gateway may need to bind to LAN / wildcard rather than loopback.

That configuration can be unusual, but it is valid. It may deserve separate exposure/security/audit guidance, but it should not appear as a port-conflict error.

Fix Action

Fix / Workaround

Tested local patch shape

A local dist patch with this source-level shape fixed the false positive without restart and preserved conflict behavior.

Local verification after patch

PR fix notes

PR #78128: [AI-assisted] fix(status): accept expected Gateway bind listeners

Description (problem / solution / changelog)

Summary

  • Add a shared expected-Gateway listener predicate that accepts a single OpenClaw Gateway listener on loopback, LAN, or wildcard bind addresses, plus the existing same-PID dual-stack loopback case.
  • Reuse that predicate in status --all, Gateway doctor diagnostics, and shared port hints so healthy managed Gateway listeners do not render as port conflicts.
  • Preserve warnings for unknown owners, SSH tunnels, mixed listeners, and multiple Gateway PIDs.

Fixes #77939.

Real behavior proof

Behavior or issue addressed: openclaw status --all / doctor could warn that port 18789 was already in use when the only listener was the expected OpenClaw Gateway bound on a LAN or wildcard address.

Real environment tested: Local OpenClaw source checkout on Windows, running the PR branch with corepack pnpm exec tsx against the actual status diagnosis and port-formatting code. The probe models the reporter's Linux listener shape (0.0.0.0:18789) because this machine does not have a live Linux/systemd LAN-bound Gateway.

Exact steps or command run after this patch:

@'
import { buildPortHints, isExpectedGatewayListeners } from "./src/infra/ports-format.ts";
import { appendStatusAllDiagnosis } from "./src/commands/status-all/diagnosis.ts";

const listeners = [
  { pid: 5001, commandLine: "node /opt/openclaw/dist/index.js gateway --bind lan --port 18789", address: "0.0.0.0:18789" },
];
const lines = [];
await appendStatusAllDiagnosis({
  lines,
  progress: { setLabel() {}, setPercent() {}, tick() {}, done() {} },
  muted: (text) => text,
  ok: (text) => text,
  warn: (text) => text,
  fail: (text) => text,
  connectionDetailsForReport: "ws://127.0.0.1:18789",
  snap: null,
  remoteUrlMissing: false,
  secretDiagnostics: [],
  sentinel: null,
  lastErr: null,
  port: 18789,
  portUsage: { port: 18789, status: "busy", listeners, hints: buildPortHints(listeners, 18789) },
  tailscaleMode: "off",
  tailscale: { backendState: null, dnsName: null, ips: [], error: null },
  tailscaleHttpsUrl: null,
  skillStatus: null,
  pluginCompatibility: [],
  channelsStatus: null,
  channelIssues: [],
  gatewayReachable: true,
  health: { ok: true, status: "live" },
  nodeOnlyGateway: null,
});
console.log(JSON.stringify({
  expectedGatewayListeners: isExpectedGatewayListeners(listeners, 18789),
  hints: buildPortHints(listeners, 18789),
  statusLines: lines.filter((line) => line.includes("Port 18789") || line.includes("Detected OpenClaw Gateway")),
}, null, 2));
 '@ | corepack pnpm exec tsx -

Evidence after fix:

{
  "expectedGatewayListeners": true,
  "hints": [],
  "statusLines": [
    "✓ Port 18789",
    "  Detected OpenClaw Gateway listener on the configured port."
  ]
}

Observed result after fix: A single OpenClaw Gateway listener on 0.0.0.0:18789 is treated as the configured Gateway, no stop/use-different-port hint is emitted, and status renders ✓ Port 18789 instead of Port 18789 is already in use.

What was not tested: I did not run a live Linux/systemd Gateway bound to a LAN interface on this Windows machine. The regression coverage exercises the same listener shape through the actual shared diagnostic code and keeps multi-process/mixed listener conflicts warning.

Validation

  • corepack pnpm exec oxfmt --check --threads=1 src/infra/ports-format.ts src/infra/ports.ts src/infra/ports-format.test.ts src/commands/status-all/diagnosis.ts src/commands/status-all/diagnosis.test.ts src/commands/doctor-gateway-daemon-flow.ts src/commands/doctor-gateway-daemon-flow.test.ts CHANGELOG.md
  • corepack pnpm exec oxlint src/infra/ports-format.ts src/infra/ports.ts src/infra/ports-format.test.ts src/commands/status-all/diagnosis.ts src/commands/status-all/diagnosis.test.ts src/commands/doctor-gateway-daemon-flow.ts src/commands/doctor-gateway-daemon-flow.test.ts
  • corepack pnpm test src/infra/ports-format.test.ts src/commands/status-all/diagnosis.test.ts src/commands/doctor-gateway-daemon-flow.test.ts
  • corepack pnpm check:changed
  • corepack pnpm check:changelog-attributions
  • git diff --check origin/main...HEAD
  • corepack pnpm exec tsgo -p tsconfig.core.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core.tsbuildinfo
  • corepack pnpm exec tsgo -p test/tsconfig/tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfo
  • full corepack pnpm lint --threads=8 from a temporary Z:\ drive mapping

AI assistance was used to prepare this patch.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/commands/doctor-gateway-daemon-flow.test.ts (modified, +40/-1)
  • src/commands/doctor-gateway-daemon-flow.ts (modified, +9/-2)
  • src/commands/status-all/diagnosis.test.ts (modified, +13/-0)
  • src/commands/status-all/diagnosis.ts (modified, +8/-1)
  • src/infra/ports-format.test.ts (modified, +32/-1)
  • src/infra/ports-format.ts (modified, +41/-2)
  • src/infra/ports.test.ts (modified, +1/-1)
  • src/infra/ports.ts (modified, +2/-0)

Code Example

GET http://127.0.0.1:18789/healthz
=> {"ok":true,"status":"live"}

ss -H -ltnp 'sport = :18789'
=> LISTEN 0.0.0.0:18789 users:(("MainThread",pid=<pid>,fd=27))

ps -p <pid> -o command=
=> /usr/bin/node .../openclaw/dist/index.js gateway --port 18789

---

function isWildcardAddress(host: string): boolean {
  return host === "0.0.0.0" || host === "::" || host === "*";
}

function isExpectedGatewayBindAddress(host: string): boolean {
  return Boolean(classifyLoopbackAddressFamily(host)) || isWildcardAddress(host);
}

export function isSingleExpectedGatewayListener(
  listeners: PortListener[],
  port: number,
): boolean {
  if (listeners.length !== 1) return false;

  const [listener] = listeners;
  if (classifyPortListener(listener, port) !== "gateway") return false;
  if (typeof listener.pid !== "number" || !Number.isFinite(listener.pid)) return false;
  if (typeof listener.address !== "string") return false;

  const parsed = parseListenerAddress(listener.address);
  if (!parsed || parsed.port !== port) return false;

  return isExpectedGatewayBindAddress(parsed.host);
}

export function isExpectedGatewayListeners(
  listeners: PortListener[],
  port: number,
): boolean {
  return (
    isSingleExpectedGatewayListener(listeners, port) ||
    isDualStackLoopbackGatewayListeners(listeners, port)
  );
}

---

if (kinds.has("gateway") && !isExpectedGatewayListeners(listeners, port)) {
  hints.push(`Gateway already running locally. Stop it (${formatCliCommand("openclaw gateway stop")}) or use a different port.`);
}

if (listeners.length > 1 && !isExpectedGatewayListeners(listeners, port)) {
  hints.push("Multiple listeners detected; ensure only one gateway/tunnel per port unless intentionally running isolated profiles.");
}

---

const expectedGatewayListeners = isExpectedGatewayListeners(params.portUsage.listeners, params.port);
const portOk = params.portUsage.listeners.length === 0 || expectedGatewayListeners;

emitCheck(`Port ${params.port}`, portOk ? "ok" : "warn");

if (!portOk) {
  for (const line of formatPortDiagnostics(params.portUsage)) lines.push(`  ${muted(line)}`);
} else if (expectedGatewayListeners && params.portUsage.listeners.length > 0) {
  lines.push(`  ${muted("Detected OpenClaw Gateway listener on the configured port.")}`);
}

---

const diagnostics = await inspectPortUsage(resolveGatewayPort(params.cfg, process.env));

if (
  diagnostics.status === "busy" &&
  !isExpectedGatewayListeners(diagnostics.listeners, diagnostics.port)
) {
  note(formatPortDiagnostics(diagnostics).join("\n"), "Gateway port");
}

---

inspectPortUsage(18789) => status: "busy", one listener address: "*:18789"
isExpectedGatewayListeners(...) => true
buildPortHints(...) => []

---

Port 18789
  Detected OpenClaw Gateway listener on the configured port.
RAW_BUFFERClick to expand / collapse

Summary

openclaw status --all / doctor currently treats a healthy managed OpenClaw Gateway listener as a port conflict when the gateway is intentionally bound on a non-loopback/LAN address for an internal reverse-proxy deployment.

This is the same false-positive class as #53398, but #53398 only covered the dual-stack loopback case (127.0.0.1 + ::1). A valid gateway.bind=lan / wildcard bind setup can still render misleading output such as:

  • Port 18789 is already in use.
  • Gateway already running locally. Stop it (...) or use a different port.

That wording is wrong when the only listener on the configured port is the expected managed OpenClaw Gateway and gateway health/liveness succeeds.

Why this matters

Some deployments intentionally do not use Tailscale and instead expose OpenClaw only through a controlled internal / allowlisted reverse-proxy path. In that setup the gateway may need to bind to LAN / wildcard rather than loopback.

That configuration can be unusual, but it is valid. It may deserve separate exposure/security/audit guidance, but it should not appear as a port-conflict error.

Local evidence from a validated setup

On a Linux/systemd install using the default gateway port:

GET http://127.0.0.1:18789/healthz
=> {"ok":true,"status":"live"}

ss -H -ltnp 'sport = :18789'
=> LISTEN 0.0.0.0:18789 users:(("MainThread",pid=<pid>,fd=27))

ps -p <pid> -o command=
=> /usr/bin/node .../openclaw/dist/index.js gateway --port 18789

The port is occupied, but by the configured, healthy OpenClaw Gateway itself. This should be rendered as OK / informational, not as a conflict.

Current root cause

The installed bundle maps to these source areas:

  • src/infra/ports-format.ts
  • src/commands/status-all/diagnosis.ts
  • src/commands/doctor-gateway-daemon-flow.ts

Current behavior:

  • buildPortHints() emits the stop/use-different-port hint for any listener classified as gateway.
  • status-all only treats an empty listener set or isDualStackLoopbackGatewayListeners(...) as OK.
  • doctor reports busy-port diagnostics without distinguishing expected Gateway listeners from foreign/conflicting owners.

Proposed behavior

Distinguish expected OpenClaw Gateway listeners from real conflicts.

OK / info:

  • no listener
  • a single OpenClaw Gateway listener on the configured port using loopback (127.0.0.1, ::1, localhost)
  • a single OpenClaw Gateway listener on the configured port using wildcard/LAN bind (0.0.0.0, ::, *)
  • the existing dual-stack loopback same-PID case from #53398

Still warn:

  • unknown process owns the port
  • SSH tunnel owns the port
  • multiple unrelated listeners
  • multiple OpenClaw Gateway PIDs on the same port
  • mixed Gateway + non-Gateway listeners

Tested local patch shape

A local dist patch with this source-level shape fixed the false positive without restart and preserved conflict behavior.

src/infra/ports-format.ts

Add shared predicates near the existing dual-stack helper:

function isWildcardAddress(host: string): boolean {
  return host === "0.0.0.0" || host === "::" || host === "*";
}

function isExpectedGatewayBindAddress(host: string): boolean {
  return Boolean(classifyLoopbackAddressFamily(host)) || isWildcardAddress(host);
}

export function isSingleExpectedGatewayListener(
  listeners: PortListener[],
  port: number,
): boolean {
  if (listeners.length !== 1) return false;

  const [listener] = listeners;
  if (classifyPortListener(listener, port) !== "gateway") return false;
  if (typeof listener.pid !== "number" || !Number.isFinite(listener.pid)) return false;
  if (typeof listener.address !== "string") return false;

  const parsed = parseListenerAddress(listener.address);
  if (!parsed || parsed.port !== port) return false;

  return isExpectedGatewayBindAddress(parsed.host);
}

export function isExpectedGatewayListeners(
  listeners: PortListener[],
  port: number,
): boolean {
  return (
    isSingleExpectedGatewayListener(listeners, port) ||
    isDualStackLoopbackGatewayListeners(listeners, port)
  );
}

Then avoid conflict hints for expected Gateway listeners:

if (kinds.has("gateway") && !isExpectedGatewayListeners(listeners, port)) {
  hints.push(`Gateway already running locally. Stop it (${formatCliCommand("openclaw gateway stop")}) or use a different port.`);
}

if (listeners.length > 1 && !isExpectedGatewayListeners(listeners, port)) {
  hints.push("Multiple listeners detected; ensure only one gateway/tunnel per port unless intentionally running isolated profiles.");
}

src/commands/status-all/diagnosis.ts

Use the shared predicate instead of the dual-stack-loopback-only check:

const expectedGatewayListeners = isExpectedGatewayListeners(params.portUsage.listeners, params.port);
const portOk = params.portUsage.listeners.length === 0 || expectedGatewayListeners;

emitCheck(`Port ${params.port}`, portOk ? "ok" : "warn");

if (!portOk) {
  for (const line of formatPortDiagnostics(params.portUsage)) lines.push(`  ${muted(line)}`);
} else if (expectedGatewayListeners && params.portUsage.listeners.length > 0) {
  lines.push(`  ${muted("Detected OpenClaw Gateway listener on the configured port.")}`);
}

src/commands/doctor-gateway-daemon-flow.ts

Suppress busy-port notes only when the listener set is expected:

const diagnostics = await inspectPortUsage(resolveGatewayPort(params.cfg, process.env));

if (
  diagnostics.status === "busy" &&
  !isExpectedGatewayListeners(diagnostics.listeners, diagnostics.port)
) {
  note(formatPortDiagnostics(diagnostics).join("\n"), "Gateway port");
}

Local verification after patch

The local patch produced:

inspectPortUsage(18789) => status: "busy", one listener address: "*:18789"
isExpectedGatewayListeners(...) => true
buildPortHints(...) => []

openclaw status --all then rendered:

✓ Port 18789
  Detected OpenClaw Gateway listener on the configured port.

No Port 18789 warning remained. Gateway health stayed live before and after the patch.

Regression tests suggested

  • single OpenClaw Gateway listener on 0.0.0.0:<port> is expected and produces no stop/use-different-port hint
  • single OpenClaw Gateway listener on [::]:<port> is expected and produces no stop/use-different-port hint
  • existing same-PID dual-stack loopback case remains expected
  • two different OpenClaw Gateway PIDs still warn
  • unknown process still warns
  • SSH tunnel still warns
  • doctor suppresses busy-port note only for expected Gateway listeners

Related

  • #53398 fixed the same false-positive class for dual-stack loopback only. This issue extends the same principle to valid LAN/wildcard reverse-proxy deployments.

extent analysis

TL;DR

Update the openclaw code to distinguish between expected OpenClaw Gateway listeners and real port conflicts by using the proposed isExpectedGatewayListeners function.

Guidance

  • Review the proposed code changes in src/infra/ports-format.ts, src/commands/status-all/diagnosis.ts, and src/commands/doctor-gateway-daemon-flow.ts to understand the suggested fix.
  • Apply the patch to your local openclaw installation and verify that it resolves the false positive port conflict issue.
  • Test the updated code with various scenarios, including single and multiple OpenClaw Gateway listeners, unknown processes, and SSH tunnels, to ensure the fix works as expected.
  • Consider adding regression tests to cover the suggested test cases to prevent similar issues in the future.

Example

The proposed code changes include adding a new function isExpectedGatewayListeners to src/infra/ports-format.ts:

export function isExpectedGatewayListeners(
  listeners: PortListener[],
  port: number,
): boolean {
  return (
    isSingleExpectedGatewayListener(listeners, port) ||
    isDualStackLoopbackGatewayListeners(listeners, port)
  );
}

This function is then used in src/commands/status-all/diagnosis.ts and src/commands/doctor-gateway-daemon-flow.ts to suppress conflict hints for expected Gateway listeners.

Notes

The proposed fix assumes that the openclaw codebase is modular and allows for easy modification of the relevant functions. If the codebase is not modular or has complex dependencies, additional refactoring may be necessary.

Recommendation

Apply the proposed patch to your local openclaw installation, as it provides a clear and targeted fix for the issue. This will allow you to distinguish between expected OpenClaw Gateway listeners and real port conflicts, resolving the false positive issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix status/doctor reports false Gateway port conflict for healthy LAN-bound gateway [1 pull requests, 2 comments, 3 participants]