fix: classify ws pre-handshake close as benign · openclaw/openclaw@f327073
akrimm702
·
2026-05-30
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -400,6 +400,12 @@ describe("isTransientUnhandledRejectionError", () => {
|
400 | 400 | const wrappedDestroyedHttp2Session = Object.assign(new Error("model call failed"), { |
401 | 401 | cause: destroyedHttp2Session, |
402 | 402 | }); |
| 403 | +const wsPreHandshakeClose = new Error( |
| 404 | +"WebSocket was closed before the connection was established", |
| 405 | +); |
| 406 | +const wrappedWsPreHandshakeClose = Object.assign(new Error("feishu reconnect failed"), { |
| 407 | +cause: wsPreHandshakeClose, |
| 408 | +}); |
403 | 409 | const generic = new Error("boom"); |
404 | 410 | |
405 | 411 | expect(isBenignUncaughtExceptionError(epipe)).toBe(true); |
@@ -414,6 +420,13 @@ describe("isTransientUnhandledRejectionError", () => {
|
414 | 420 | expect(isBenignUncaughtExceptionError(destroyedHttp2Session)).toBe(true); |
415 | 421 | expect(isBenignUncaughtExceptionError(wrappedDestroyedHttp2Session)).toBe(true); |
416 | 422 | expect(isBenignUncaughtExceptionError(new Error("ERR_HTTP2_INVALID_SESSION"))).toBe(true); |
| 423 | +expect(isBenignUncaughtExceptionError(wsPreHandshakeClose)).toBe(true); |
| 424 | +expect(isBenignUncaughtExceptionError(wrappedWsPreHandshakeClose)).toBe(true); |
| 425 | +expect( |
| 426 | +isBenignUncaughtExceptionError( |
| 427 | +new Error("WebSocket error: WebSocket was closed before the connection was established"), |
| 428 | +), |
| 429 | +).toBe(false); |
417 | 430 | expect(isBenignUncaughtExceptionError(generic)).toBe(false); |
418 | 431 | }); |
419 | 432 | it("returns true for transient SQLite errors", () => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -111,6 +111,7 @@ const TRANSIENT_NETWORK_MESSAGE_CODE_RE =
|
111 | 111 | /\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; |
112 | 112 | const BENIGN_UNCAUGHT_EXCEPTION_NETWORK_MESSAGE_CODE_RE = |
113 | 113 | /\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; |
| 114 | +const WS_PRE_HANDSHAKE_CLOSE_MESSAGE = "websocket was closed before the connection was established"; |
114 | 115 | |
115 | 116 | const TRANSIENT_SQLITE_MESSAGE_CODE_RE = |
116 | 117 | /\b(SQLITE_BUSY|SQLITE_CANTOPEN|SQLITE_IOERR|SQLITE_LOCKED)\b/i; |
@@ -176,6 +177,16 @@ function isWrappedFetchFailedMessage(message: string): boolean {
|
176 | 177 | return /:\s*fetch failed$/.test(message); |
177 | 178 | } |
178 | 179 | |
| 180 | +function isBenignUncaughtNetworkMessage(message: string): boolean { |
| 181 | +if (BENIGN_UNCAUGHT_EXCEPTION_NETWORK_MESSAGE_CODE_RE.test(message)) { |
| 182 | +return true; |
| 183 | +} |
| 184 | + |
| 185 | +// `ws` emits this exact Error when close()/terminate() aborts a CONNECTING socket. |
| 186 | +// Keep exact matching so arbitrary WebSocket errors still take the fatal path. |
| 187 | +return message === WS_PRE_HANDSHAKE_CLOSE_MESSAGE; |
| 188 | +} |
| 189 | + |
179 | 190 | function getErrorCause(err: unknown): unknown { |
180 | 191 | if (!err || typeof err !== "object") { |
181 | 192 | return undefined; |
@@ -437,7 +448,7 @@ function isBenignUncaughtNetworkException(err: unknown): boolean {
|
437 | 448 | continue; |
438 | 449 | } |
439 | 450 | const message = normalizeLowercaseStringOrEmpty((candidate as { message?: unknown }).message); |
440 | | -if (message && BENIGN_UNCAUGHT_EXCEPTION_NETWORK_MESSAGE_CODE_RE.test(message)) { |
| 451 | +if (message && isBenignUncaughtNetworkMessage(message)) { |
441 | 452 | return true; |
442 | 453 | } |
443 | 454 | } |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。