From c0d4abc59eed2d0cd7d71eba9a28db2f75ccf682 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Mar 2026 14:27:58 -0700 Subject: [PATCH] fix(gateway): suppress ciao interface assertions Closes #38628. Refs #47159, #52431. Co-authored-by: Peter Steinberger --- CHANGELOG.md | 1 + src/infra/bonjour-ciao.test.ts | 69 ++++++++++++++++++++++------------ src/infra/bonjour-ciao.ts | 18 ++++++--- src/infra/bonjour.ts | 4 +- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be7e3709075..98af9321ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/infra/bonjour-ciao.test.ts b/src/infra/bonjour-ciao.test.ts index 3d9ec83f0f4..ad45974e2fc 100644 --- a/src/infra/bonjour-ciao.test.ts +++ b/src/infra/bonjour-ciao.test.ts @@ -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(); }); }); diff --git a/src/infra/bonjour-ciao.ts b/src/infra/bonjour-ciao.ts index f39902c0aa7..71feebd60bc 100644 --- a/src/infra/bonjour-ciao.ts +++ b/src/infra/bonjour-ciao.ts @@ -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; } diff --git a/src/infra/bonjour.ts b/src/infra/bonjour.ts index 457853a9b45..a4a21a60405 100644 --- a/src/infra/bonjour.ts +++ b/src/infra/bonjour.ts @@ -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 };