Merge 2f70888a77 into 09be5d45d5
This commit is contained in:
commit
6f417fca69
10
CHANGELOG.md
10
CHANGELOG.md
@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
Docs: https://docs.openclaw.ai
|
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
|
## 2026.1.29
|
||||||
Status: stable.
|
Status: stable.
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,7 @@ type GatewayRunOpts = {
|
|||||||
rawStreamPath?: unknown;
|
rawStreamPath?: unknown;
|
||||||
dev?: boolean;
|
dev?: boolean;
|
||||||
reset?: boolean;
|
reset?: boolean;
|
||||||
|
bonjour?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const gatewayLog = createSubsystemLogger("gateway");
|
const gatewayLog = createSubsystemLogger("gateway");
|
||||||
@ -89,6 +90,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
|||||||
process.env.OPENCLAW_RAW_STREAM_PATH = rawStreamPath;
|
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) {
|
if (devMode) {
|
||||||
await ensureDevGatewayConfig({ reset: Boolean(opts.reset) });
|
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("--compact", 'Alias for "--ws-log compact"', false)
|
||||||
.option("--raw-stream", "Log raw model stream events to jsonl", false)
|
.option("--raw-stream", "Log raw model stream events to jsonl", false)
|
||||||
.option("--raw-stream-path <path>", "Raw stream jsonl path")
|
.option("--raw-stream-path <path>", "Raw stream jsonl path")
|
||||||
|
.option("--no-bonjour", "Disable mDNS/Bonjour service advertising", false)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await runGatewayCommand(opts);
|
await runGatewayCommand(opts);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,11 +2,32 @@ import { logDebug } from "../logger.js";
|
|||||||
|
|
||||||
import { formatBonjourError } from "./bonjour-errors.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 {
|
export function ignoreCiaoCancellationRejection(reason: unknown): boolean {
|
||||||
const message = formatBonjourError(reason).toUpperCase();
|
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)
|
||||||
|
// Note: The error message typically contains "MDNSServer" in the stack trace
|
||||||
|
const isAssertionError = errorName === "ASSERTIONERROR" && message.includes("MDNSSERVER");
|
||||||
|
|
||||||
|
if (!isTransientError && !isAssertionError) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
logDebug(`bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`);
|
logDebug(`bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/infra/bonjour-errors.integration.test.ts
Normal file
94
src/infra/bonjour-errors.integration.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -158,5 +158,32 @@ describe("installUnhandledRejectionHandler - fatal detection", () => {
|
|||||||
expect(exitCalls).toEqual([]);
|
expect(exitCalls).toEqual([]);
|
||||||
expect(consoleWarnSpy).toHaveBeenCalled();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -34,6 +34,8 @@ const TRANSIENT_NETWORK_CODES = new Set([
|
|||||||
"UND_ERR_SOCKET",
|
"UND_ERR_SOCKET",
|
||||||
"UND_ERR_HEADERS_TIMEOUT",
|
"UND_ERR_HEADERS_TIMEOUT",
|
||||||
"UND_ERR_BODY_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 {
|
function getErrorCause(err: unknown): unknown {
|
||||||
@ -99,6 +101,23 @@ export function isTransientNetworkError(err: unknown): boolean {
|
|||||||
return err.errors.some((e) => isTransientNetworkError(e));
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user