mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 02:14:02 +00:00
fix: classify ws pre-handshake close as benign
Classify the exact `ws` pre-handshake close-before-open error as a benign uncaught network exception so transient Feishu WebSocket cleanup does not crash the gateway process. The classifier now keeps the upstream `ws` message as an exact contract and rejects broader prefixed WebSocket messages, with regression coverage for direct, wrapped, and non-exact cases. Fixes #88257. Thanks @akrimm702. Co-authored-by: AI-HUB <144416483+akrimm702@users.noreply.github.com>
This commit is contained in:
@@ -400,6 +400,12 @@ describe("isTransientUnhandledRejectionError", () => {
|
||||
const wrappedDestroyedHttp2Session = Object.assign(new Error("model call failed"), {
|
||||
cause: destroyedHttp2Session,
|
||||
});
|
||||
const wsPreHandshakeClose = new Error(
|
||||
"WebSocket was closed before the connection was established",
|
||||
);
|
||||
const wrappedWsPreHandshakeClose = Object.assign(new Error("feishu reconnect failed"), {
|
||||
cause: wsPreHandshakeClose,
|
||||
});
|
||||
const generic = new Error("boom");
|
||||
|
||||
expect(isBenignUncaughtExceptionError(epipe)).toBe(true);
|
||||
@@ -414,6 +420,13 @@ describe("isTransientUnhandledRejectionError", () => {
|
||||
expect(isBenignUncaughtExceptionError(destroyedHttp2Session)).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(wrappedDestroyedHttp2Session)).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(new Error("ERR_HTTP2_INVALID_SESSION"))).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(wsPreHandshakeClose)).toBe(true);
|
||||
expect(isBenignUncaughtExceptionError(wrappedWsPreHandshakeClose)).toBe(true);
|
||||
expect(
|
||||
isBenignUncaughtExceptionError(
|
||||
new Error("WebSocket error: WebSocket was closed before the connection was established"),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(isBenignUncaughtExceptionError(generic)).toBe(false);
|
||||
});
|
||||
it("returns true for transient SQLite errors", () => {
|
||||
|
||||
@@ -111,6 +111,7 @@ const TRANSIENT_NETWORK_MESSAGE_CODE_RE =
|
||||
/\b(ECONNRESET|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED|EPIPE|ENETDOWN|EHOSTUNREACH|ENETUNREACH|EADDRNOTAVAIL|EAI_AGAIN|EPROTO|UND_ERR_CONNECT_TIMEOUT|UND_ERR_DNS_RESOLVE_FAILED|UND_ERR_CONNECT|UND_ERR_SOCKET|UND_ERR_HEADERS_TIMEOUT|UND_ERR_BODY_TIMEOUT|ERR_HTTP2_INVALID_SESSION)\b/i;
|
||||
const BENIGN_UNCAUGHT_EXCEPTION_NETWORK_MESSAGE_CODE_RE =
|
||||
/\b(ECONNREFUSED|ENETDOWN|EHOSTUNREACH|ENETUNREACH|EADDRNOTAVAIL|EAI_AGAIN|ENOTFOUND|ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|UND_ERR_DNS_RESOLVE_FAILED|UND_ERR_CONNECT|ERR_HTTP2_INVALID_SESSION)\b/i;
|
||||
const WS_PRE_HANDSHAKE_CLOSE_MESSAGE = "websocket was closed before the connection was established";
|
||||
|
||||
const TRANSIENT_SQLITE_MESSAGE_CODE_RE =
|
||||
/\b(SQLITE_BUSY|SQLITE_CANTOPEN|SQLITE_IOERR|SQLITE_LOCKED)\b/i;
|
||||
@@ -176,6 +177,16 @@ function isWrappedFetchFailedMessage(message: string): boolean {
|
||||
return /:\s*fetch failed$/.test(message);
|
||||
}
|
||||
|
||||
function isBenignUncaughtNetworkMessage(message: string): boolean {
|
||||
if (BENIGN_UNCAUGHT_EXCEPTION_NETWORK_MESSAGE_CODE_RE.test(message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `ws` emits this exact Error when close()/terminate() aborts a CONNECTING socket.
|
||||
// Keep exact matching so arbitrary WebSocket errors still take the fatal path.
|
||||
return message === WS_PRE_HANDSHAKE_CLOSE_MESSAGE;
|
||||
}
|
||||
|
||||
function getErrorCause(err: unknown): unknown {
|
||||
if (!err || typeof err !== "object") {
|
||||
return undefined;
|
||||
@@ -437,7 +448,7 @@ function isBenignUncaughtNetworkException(err: unknown): boolean {
|
||||
continue;
|
||||
}
|
||||
const message = normalizeLowercaseStringOrEmpty((candidate as { message?: unknown }).message);
|
||||
if (message && BENIGN_UNCAUGHT_EXCEPTION_NETWORK_MESSAGE_CODE_RE.test(message)) {
|
||||
if (message && isBenignUncaughtNetworkMessage(message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user