fix(gateway): suppress ciao interface assertions

Closes #38628.
Refs #47159, #52431.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Peter Steinberger 2026-03-22 14:27:58 -07:00
parent 3faaf8984f
commit c0d4abc59e
No known key found for this signature in database
4 changed files with 62 additions and 30 deletions

View File

@ -257,6 +257,7 @@ Docs: https://docs.openclaw.ai
<<<<<<< HEAD
- WhatsApp/reconnect: preserve the last inbound timestamp across reconnect attempts so the watchdog can still recycle linked-but-dead listeners after a restart instead of leaving them stuck connected forever.
- Gateway/network discovery: guard LAN, tailnet, and pairing interface enumeration so WSL2 and restricted hosts degrade to missing-address fallbacks instead of crashing on `uv_interface_addresses` errors. (#44180, #47590)
- Gateway/bonjour: suppress the non-fatal `@homebridge/ciao` IPv4-loss assertion during interface churn so WiFi/VPN/sleep-wake changes no longer take down the gateway. (#38628, #47159, #52431)
### Breaking

View File

@ -1,47 +1,70 @@
import { describe, expect, it, vi } from "vitest";
const logDebugMock = vi.hoisted(() => vi.fn());
const logWarnMock = vi.hoisted(() => vi.fn());
vi.mock("../logger.js", () => ({
logDebug: (...args: unknown[]) => logDebugMock(...args),
logWarn: (...args: unknown[]) => logWarnMock(...args),
}));
const { ignoreCiaoCancellationRejection } = await import("./bonjour-ciao.js");
const { ignoreCiaoUnhandledRejection } = await import("./bonjour-ciao.js");
describe("bonjour-ciao", () => {
it("ignores and logs ciao announcement cancellation rejections", () => {
expect(
ignoreCiaoCancellationRejection(new Error("Ciao announcement cancelled by shutdown")),
).toBe(true);
expect(logDebugMock).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
});
it("ignores and logs ciao probing cancellation rejections", () => {
logDebugMock.mockReset();
expect(ignoreCiaoCancellationRejection(new Error("CIAO PROBING CANCELLED"))).toBe(true);
expect(logDebugMock).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
});
it("ignores lower-case string cancellation reasons too", () => {
logDebugMock.mockReset();
expect(ignoreCiaoCancellationRejection("ciao announcement cancelled during cleanup")).toBe(
expect(ignoreCiaoUnhandledRejection(new Error("Ciao announcement cancelled by shutdown"))).toBe(
true,
);
expect(logDebugMock).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
expect(logWarnMock).not.toHaveBeenCalled();
});
it("ignores and logs ciao probing cancellation rejections", () => {
logDebugMock.mockReset();
logWarnMock.mockReset();
expect(ignoreCiaoUnhandledRejection(new Error("CIAO PROBING CANCELLED"))).toBe(true);
expect(logDebugMock).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
expect(logWarnMock).not.toHaveBeenCalled();
});
it("ignores lower-case string cancellation reasons too", () => {
logDebugMock.mockReset();
logWarnMock.mockReset();
expect(ignoreCiaoUnhandledRejection("ciao announcement cancelled during cleanup")).toBe(true);
expect(logDebugMock).toHaveBeenCalledWith(
expect.stringContaining("ignoring unhandled ciao rejection"),
);
expect(logWarnMock).not.toHaveBeenCalled();
});
it("suppresses ciao interface assertion rejections as non-fatal", () => {
logDebugMock.mockReset();
logWarnMock.mockReset();
const error = Object.assign(
new Error("Reached illegal state! IPV4 address change from defined to undefined!"),
{ name: "AssertionError" },
);
expect(ignoreCiaoUnhandledRejection(error)).toBe(true);
expect(logWarnMock).toHaveBeenCalledWith(
expect.stringContaining("suppressing ciao interface assertion"),
);
expect(logDebugMock).not.toHaveBeenCalled();
});
it("keeps unrelated rejections visible", () => {
logDebugMock.mockReset();
logWarnMock.mockReset();
expect(ignoreCiaoCancellationRejection(new Error("boom"))).toBe(false);
expect(ignoreCiaoUnhandledRejection(new Error("boom"))).toBe(false);
expect(logDebugMock).not.toHaveBeenCalled();
expect(logWarnMock).not.toHaveBeenCalled();
});
});

View File

@ -1,13 +1,21 @@
import { logDebug } from "../logger.js";
import { logDebug, logWarn } from "../logger.js";
import { formatBonjourError } from "./bonjour-errors.js";
const CIAO_CANCELLATION_MESSAGE_RE = /^CIAO (?:ANNOUNCEMENT|PROBING) CANCELLED\b/u;
const CIAO_INTERFACE_ASSERTION_MESSAGE_RE =
/REACHED ILLEGAL STATE!?\s+IPV4 ADDRESS CHANGE FROM DEFINED TO UNDEFINED!?/u;
export function ignoreCiaoCancellationRejection(reason: unknown): boolean {
const message = formatBonjourError(reason).toUpperCase();
export function ignoreCiaoUnhandledRejection(reason: unknown): boolean {
const formatted = formatBonjourError(reason);
const message = formatted.toUpperCase();
if (!CIAO_CANCELLATION_MESSAGE_RE.test(message)) {
return false;
if (!CIAO_INTERFACE_ASSERTION_MESSAGE_RE.test(message)) {
return false;
}
logWarn(`bonjour: suppressing ciao interface assertion: ${formatted}`);
return true;
}
logDebug(`bonjour: ignoring unhandled ciao rejection: ${formatBonjourError(reason)}`);
logDebug(`bonjour: ignoring unhandled ciao rejection: ${formatted}`);
return true;
}

View File

@ -1,6 +1,6 @@
import { logDebug, logWarn } from "../logger.js";
import { getLogger } from "../logging.js";
import { ignoreCiaoCancellationRejection } from "./bonjour-ciao.js";
import { ignoreCiaoUnhandledRejection } from "./bonjour-ciao.js";
import { formatBonjourError } from "./bonjour-errors.js";
import { isTruthyEnvValue } from "./env.js";
import { registerUnhandledRejectionHandler } from "./unhandled-rejections.js";
@ -175,7 +175,7 @@ export async function startGatewayBonjourAdvertiser(
const cleanupUnhandledRejection =
services.length > 0
? registerUnhandledRejectionHandler(ignoreCiaoCancellationRejection)
? registerUnhandledRejectionHandler(ignoreCiaoUnhandledRejection)
: undefined;
return { responder, services, cleanupUnhandledRejection };