diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5192d724a..f3002819abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ Docs: https://docs.openclaw.ai - Cron/Schedule errors: notify users when a job is auto-disabled after repeated schedule computation failures. (#29098) Thanks . - File tools/tilde paths: expand `~/...` against the user home directory before workspace-root checks in host file read/write/edit paths, while preserving root-boundary enforcement so outside-root targets remain blocked. (#29779) Thanks @Glucksberg. - Slack/HTTP mode startup: treat Slack HTTP accounts as configured when `botToken` + `signingSecret` are present (without requiring `appToken`) in channel config/runtime status so webhook mode is not silently skipped. (#30567) +- Slack/Transient request errors: classify Slack request-error messages like `Client network socket disconnected before secure TLS connection was established` as transient in unhandled-rejection fatal detection, preventing temporary network drops from crash-looping the gateway. (#23169) - Slack/Usage footer formatting: wrap session keys in inline code in full response-usage footers so Slack does not parse colon-delimited session segments as emoji shortcodes. (#30258) Thanks @pushkarsingh32. - Slack/Socket Mode slash startup: treat `app.options()` registration as best-effort and fall back to static arg menus when listener registration fails, preventing Slack monitor startup crash loops on receiver init edge cases. (#21715) - Slack/Legacy streaming config: map boolean `channels.slack.streaming=false` to unified streaming mode `off` (with `nativeStreaming=false`) so legacy configs correctly disable draft preview/native streaming instead of defaulting to `partial`. (#25990) Thanks @chilu18. diff --git a/src/infra/unhandled-rejections.fatal-detection.test.ts b/src/infra/unhandled-rejections.fatal-detection.test.ts index 7849688eb49..1a4ff61879d 100644 --- a/src/infra/unhandled-rejections.fatal-detection.test.ts +++ b/src/infra/unhandled-rejections.fatal-detection.test.ts @@ -93,10 +93,22 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { Object.assign(new Error("DNS resolve failed"), { code: "UND_ERR_DNS_RESOLVE_FAILED" }), Object.assign(new Error("Connection reset"), { code: "ECONNRESET" }), Object.assign(new Error("Timeout"), { code: "ETIMEDOUT" }), + Object.assign( + new Error( + "A request error occurred: Client network socket disconnected before secure TLS connection was established", + ), + { code: "slack_webapi_request_error" }, + ), Object.assign(new Error("A request error occurred: getaddrinfo EAI_AGAIN slack.com"), { code: "slack_webapi_request_error", original: { code: "EAI_AGAIN", syscall: "getaddrinfo", hostname: "slack.com" }, }), + Object.assign(new Error("A request error occurred: unknown"), { + code: "slack_webapi_request_error", + original: Object.assign(new Error("connect timeout"), { + code: "UND_ERR_CONNECT_TIMEOUT", + }), + }), ]; for (const transientErr of transientCases) { @@ -119,6 +131,17 @@ describe("installUnhandledRejectionHandler - fatal detection", () => { ); }); + it("exits on non-transient Slack request errors", () => { + const slackErr = Object.assign( + new Error("A request error occurred: invalid request payload"), + { + code: "slack_webapi_request_error", + }, + ); + + expectExitCodeFromUnhandled(slackErr, [1]); + }); + it("does not exit on AbortError and logs suppression warning", () => { const abortErr = new Error("This operation was aborted"); abortErr.name = "AbortError"; diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index fd3f3c966e7..03bbb003af6 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -49,6 +49,7 @@ const TRANSIENT_NETWORK_MESSAGE_CODE_RE = const TRANSIENT_NETWORK_MESSAGE_SNIPPETS = [ "getaddrinfo", "socket hang up", + "client network socket disconnected before secure tls connection was established", "network error", "network is unreachable", "temporary failure in name resolution",