From 6454842bd31d010fc4301b2ba51f2eb02fb08fea Mon Sep 17 00:00:00 2001 From: AyedAlmudarra Date: Fri, 30 Jan 2026 12:09:55 +0300 Subject: [PATCH 1/3] fix(gateway): improve crash resilience for mDNS and network errors This commit addresses several gateway crash issues: 1. Expand Bonjour error handling (bonjour-ciao.ts): - Add patterns for transient mDNS errors: - 'REACHED ILLEGAL STATE' (IPv4 address changes) - 'IPV4/IPV6 ADDRESS CHANGED' - 'NETWORK INTERFACE' changes - Handle AssertionError from MDNServer during network churn - Fixes #3821 - mDNS crash loops on sleep/wake 2. Improve network error detection (unhandled-rejections.ts): - Add ERR_ASSERTION to transient network codes - Detect AssertionError + 'IPV4 ADDRESS CHANGED' pattern - Better handling of @homebridge/ciao errors - Fixes #3815 - unhandled fetch failures crashing gateway 3. Add --no-bonjour CLI flag (gateway-cli/run.ts): - New option to disable mDNS/Bonjour advertising - Sets OPENCLAW_DISABLE_BONJOUR=1 env var - Provides workaround for #3821 while root cause is fixed Closes #3821, #3815 --- src/cli/gateway-cli/run.ts | 7 +++++++ src/infra/bonjour-ciao.ts | 22 +++++++++++++++++++++- src/infra/unhandled-rejections.ts | 19 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index d5e1f4eaa..eb07f9ce2 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -48,6 +48,7 @@ type GatewayRunOpts = { rawStreamPath?: unknown; dev?: boolean; reset?: boolean; + bonjour?: boolean; }; const gatewayLog = createSubsystemLogger("gateway"); @@ -89,6 +90,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) { process.env.OPENCLAW_RAW_STREAM_PATH = rawStreamPath; } + // Handle --no-bonjour flag to disable mDNS/Bonjour advertising + if (opts.bonjour === false) { + process.env.OPENCLAW_DISABLE_BONJOUR = "1"; + } + if (devMode) { await ensureDevGatewayConfig({ reset: Boolean(opts.reset) }); } @@ -350,6 +356,7 @@ export function addGatewayRunCommand(cmd: Command): Command { .option("--compact", 'Alias for "--ws-log compact"', false) .option("--raw-stream", "Log raw model stream events to jsonl", false) .option("--raw-stream-path ", "Raw stream jsonl path") + .option("--no-bonjour", "Disable mDNS/Bonjour service advertising", false) .action(async (opts) => { await runGatewayCommand(opts); }); diff --git a/src/infra/bonjour-ciao.ts b/src/infra/bonjour-ciao.ts index 17df4e78c..401669df1 100644 --- a/src/infra/bonjour-ciao.ts +++ b/src/infra/bonjour-ciao.ts @@ -2,11 +2,31 @@ import { logDebug } from "../logger.js"; import { formatBonjourError } from "./bonjour-errors.js"; +// Error patterns from @homebridge/ciao that should not crash the gateway +// These are typically transient network/mDNS issues that resolve on their own +const BONJOUR_TRANSIENT_ERRORS = [ + "CIAO ANNOUNCEMENT CANCELLED", + "REACHED ILLEGAL STATE", // IPv4 address changes during network interface churn + "IPV4 ADDRESS CHANGED", + "IPV6 ADDRESS CHANGED", + "MDNSSERVER", + "NETWORK INTERFACE", // Network interface changes (sleep/wake, WiFi reconnect) +]; + export function ignoreCiaoCancellationRejection(reason: unknown): boolean { const message = formatBonjourError(reason).toUpperCase(); - if (!message.includes("CIAO ANNOUNCEMENT CANCELLED")) { + const errorName = reason instanceof Error ? reason.name?.toUpperCase() : ""; + + // Check for transient mDNS/Bonjour error patterns + const isTransientError = BONJOUR_TRANSIENT_ERRORS.some((pattern) => message.includes(pattern)); + + // Also catch AssertionError from MDNServer (common during network changes) + const isAssertionError = errorName === "ASSERTIONERROR" && message.includes("MDNS"); + + if (!isTransientError && !isAssertionError) { return false; } + logDebug(`bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`); return true; } diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index 4d2a48d23..1835849cb 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -34,6 +34,8 @@ const TRANSIENT_NETWORK_CODES = new Set([ "UND_ERR_SOCKET", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT", + // mDNS/Bonjour errors from @homebridge/ciao + "ERR_ASSERTION", // IPv4 address changes during network interface churn ]); function getErrorCause(err: unknown): unknown { @@ -99,6 +101,23 @@ export function isTransientNetworkError(err: unknown): boolean { return err.errors.some((e) => isTransientNetworkError(e)); } + // Handle mDNS/Bonjour errors from @homebridge/ciao + // These are typically triggered by network interface changes (sleep/wake, WiFi reconnect) + if (err instanceof Error) { + const errorName = err.name?.toUpperCase() || ""; + const message = err.message?.toUpperCase() || ""; + + // AssertionError from MDNServer during network interface changes + if (errorName === "ASSERTIONERROR" && message.includes("IPV4 ADDRESS CHANGED")) { + return true; + } + + // Other mDNS-related transient errors + if (message.includes("MDNSSERVER") && message.includes("ILLEGAL STATE")) { + return true; + } + } + return false; } From 85294f19f70c7fb178a58b90c7b396eeec0f4f2d Mon Sep 17 00:00:00 2001 From: AyedAlmudarra Date: Fri, 30 Jan 2026 12:11:19 +0300 Subject: [PATCH 2/3] test: add mDNS error handling tests Add tests for new mDNS/Bonjour error handling: - IPv4 address change errors (AssertionError) - MDNSServer illegal state errors Verifies fixes for #3821 - gateway no longer crashes on mDNS errors --- ...handled-rejections.fatal-detection.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/infra/unhandled-rejections.fatal-detection.test.ts b/src/infra/unhandled-rejections.fatal-detection.test.ts index e991c67c9..275d861c9 100644 --- a/src/infra/unhandled-rejections.fatal-detection.test.ts +++ b/src/infra/unhandled-rejections.fatal-detection.test.ts @@ -158,5 +158,32 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { expect(exitCalls).toEqual([]); expect(consoleWarnSpy).toHaveBeenCalled(); }); + + it("does NOT exit on mDNS/Bonjour IPv4 address change errors", () => { + const mdnsErr = Object.assign( + new Error("Reached illegal state! IPv4 address changed from undefined to defined!"), + { + name: "AssertionError", + code: "ERR_ASSERTION", + }, + ); + + process.emit("unhandledRejection", mdnsErr, Promise.resolve()); + + expect(exitCalls).toEqual([]); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "[openclaw] Non-fatal unhandled rejection (continuing):", + expect.stringContaining("IPv4 address changed"), + ); + }); + + it("does NOT exit on mDNS MDNSServer illegal state errors", () => { + const mdnsErr = new Error("MDNSServer: Reached illegal state during network update"); + + process.emit("unhandledRejection", mdnsErr, Promise.resolve()); + + expect(exitCalls).toEqual([]); + expect(consoleWarnSpy).toHaveBeenCalled(); + }); }); }); From 2f70888a77628fe6a04778270d414edbd91c84ad Mon Sep 17 00:00:00 2001 From: AyedAlmudarra Date: Fri, 30 Jan 2026 15:44:53 +0300 Subject: [PATCH 3/3] feat(gateway): add integration test and changelog for mDNS fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add bonjour-errors.integration.test.ts with 8 tests - Update CHANGELOG.md with unreleased section documenting fixes - Fix MDNS → MDNSSERVER pattern in bonjour-ciao.ts Completes recommendations for #3821, #3815, #4501 --- CHANGELOG.md | 10 +++ src/infra/bonjour-ciao.ts | 3 +- src/infra/bonjour-errors.integration.test.ts | 94 ++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/infra/bonjour-errors.integration.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c0549c16..c09867c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ Docs: https://docs.openclaw.ai +## Unreleased + +### Fixes +- Gateway: fix crash resilience for mDNS/Bonjour errors during network interface changes (sleep/wake, WiFi reconnect). (#3821) + - Added error patterns for "IPv4 address changed" and "illegal state" assertions from @homebridge/ciao. + - Added `--no-bonjour` CLI flag to disable mDNS advertising entirely. +- Gateway: improve handling of transient network errors to prevent crashes. (#3815, #4501) + - Added ERR_ASSERTION to transient network error codes. + - Better detection of mDNS-related assertion errors. + ## 2026.1.29 Status: stable. diff --git a/src/infra/bonjour-ciao.ts b/src/infra/bonjour-ciao.ts index 401669df1..6bd99d9f1 100644 --- a/src/infra/bonjour-ciao.ts +++ b/src/infra/bonjour-ciao.ts @@ -21,7 +21,8 @@ export function ignoreCiaoCancellationRejection(reason: unknown): boolean { const isTransientError = BONJOUR_TRANSIENT_ERRORS.some((pattern) => message.includes(pattern)); // Also catch AssertionError from MDNServer (common during network changes) - const isAssertionError = errorName === "ASSERTIONERROR" && message.includes("MDNS"); + // Note: The error message typically contains "MDNSServer" in the stack trace + const isAssertionError = errorName === "ASSERTIONERROR" && message.includes("MDNSSERVER"); if (!isTransientError && !isAssertionError) { return false; diff --git a/src/infra/bonjour-errors.integration.test.ts b/src/infra/bonjour-errors.integration.test.ts new file mode 100644 index 000000000..8deed284e --- /dev/null +++ b/src/infra/bonjour-errors.integration.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import process from "node:process"; + +import { + installUnhandledRejectionHandler, + isTransientNetworkError, + isAbortError, +} from "./unhandled-rejections.js"; +import { ignoreCiaoCancellationRejection } from "./bonjour-ciao.js"; +import { registerUnhandledRejectionHandler } from "./unhandled-rejections.js"; + +describe("mDNS error handling integration", () => { + let originalExit: typeof process.exit; + + beforeAll(() => { + originalExit = process.exit.bind(process); + installUnhandledRejectionHandler(); + registerUnhandledRejectionHandler(ignoreCiaoCancellationRejection); + }); + + afterAll(() => { + process.exit = originalExit; + }); + + describe("error detection functions", () => { + it("detects IPv4 address change as transient network error", () => { + const mdnsError = Object.assign( + new Error("Reached illegal state! IPv4 address changed from undefined to defined!"), + { + name: "AssertionError", + code: "ERR_ASSERTION", + }, + ); + + expect(isTransientNetworkError(mdnsError)).toBe(true); + }); + + it("detects MDNSServer illegal state as transient network error", () => { + const mdnsError = new Error("MDNSServer: Reached illegal state during network update"); + + expect(isTransientNetworkError(mdnsError)).toBe(true); + }); + + it("detects AbortError correctly", () => { + const abortError = Object.assign(new Error("This operation was aborted"), { + name: "AbortError", + }); + + expect(isAbortError(abortError)).toBe(true); + }); + + it("does not treat fatal errors as transient", () => { + const fatalError = Object.assign(new Error("Out of memory"), { + code: "ERR_OUT_OF_MEMORY", + }); + + expect(isTransientNetworkError(fatalError)).toBe(false); + }); + }); + + describe("ciao cancellation handler", () => { + it("handles CIAO announcement cancelled error", () => { + const ciaoError = new Error("CIAO announcement cancelled due to network change"); + + expect(ignoreCiaoCancellationRejection(ciaoError)).toBe(true); + }); + + it("handles IPv4 address change error", () => { + const mdnsError = Object.assign( + new Error("Reached illegal state! IPv4 address changed from undefined to defined!"), + { + name: "AssertionError", + code: "ERR_ASSERTION", + }, + ); + + expect(ignoreCiaoCancellationRejection(mdnsError)).toBe(true); + }); + + it("handles MDNSServer illegal state error", () => { + const mdnsError = Object.assign(new Error("MDNSServer: Reached illegal state"), { + name: "AssertionError", + }); + + expect(ignoreCiaoCancellationRejection(mdnsError)).toBe(true); + }); + + it("does not handle unrelated errors", () => { + const genericError = new Error("Something went wrong"); + + expect(ignoreCiaoCancellationRejection(genericError)).toBe(false); + }); + }); +});