diff --git a/CHANGELOG.md b/CHANGELOG.md index 7472561f20e..e7afed7fafe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ Docs: https://docs.openclaw.ai - Plugins/CLI: make `openclaw plugins enable` and plugin install/link flows update allowlists via shared plugin-enable policy so enabled plugins are not left disabled by allowlist mismatch. (#23190) Thanks @downwind7clawd-ctrl. - Memory/QMD: add optional `memory.qmd.mcporter` search routing so QMD `query/search/vsearch` can run through mcporter keep-alive flows (including multi-collection paths) to reduce cold starts, while keeping searches on agent-scoped QMD state for consistent recall. (#19617) Thanks @nicole-luxe and @vignesh07. - Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:]]`, `[[audio_as_voice]]`) from displayed chat history, live chat event output, and session preview snippets so control tags no longer leak into user-visible surfaces. +- Infra/Network: classify undici `TypeError: fetch failed` as transient in unhandled-rejection detection even when nested causes are unclassified, preventing avoidable gateway crash loops on flaky networks. (#14345) Thanks @Unayung. - Telegram/Retry: classify undici `TypeError: fetch failed` as recoverable in both polling and send retry paths so transient fetch failures no longer fail fast. (#16699) thanks @Glucksberg. - BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines. - BlueBubbles/Private API cache: treat unknown (`null`) private-API cache status as disabled for send/attachment/reply flows to avoid stale-cache 500s, and log a warning when reply/effect features are requested while capability is unknown. (#23459) Thanks @echoVic. diff --git a/src/infra/unhandled-rejections.test.ts b/src/infra/unhandled-rejections.test.ts index a47c6d01a86..1770209f41e 100644 --- a/src/infra/unhandled-rejections.test.ts +++ b/src/infra/unhandled-rejections.test.ts @@ -79,6 +79,12 @@ describe("isTransientNetworkError", () => { expect(isTransientNetworkError(error)).toBe(true); }); + it("returns true for fetch failed with unclassified cause", () => { + const cause = Object.assign(new Error("unknown socket state"), { code: "UNKNOWN" }); + const error = Object.assign(new TypeError("fetch failed"), { cause }); + expect(isTransientNetworkError(error)).toBe(true); + }); + it("returns true for nested cause chain with network error", () => { const innerCause = Object.assign(new Error("connection reset"), { code: "ECONNRESET" }); const outerCause = Object.assign(new Error("wrapper"), { cause: innerCause }); diff --git a/src/infra/unhandled-rejections.ts b/src/infra/unhandled-rejections.ts index c2e8d935cfd..f20fd34409a 100644 --- a/src/infra/unhandled-rejections.ts +++ b/src/infra/unhandled-rejections.ts @@ -94,12 +94,10 @@ export function isTransientNetworkError(err: unknown): boolean { return true; } - // "fetch failed" TypeError from undici (Node's native fetch) + // "fetch failed" TypeError from undici (Node's native fetch). + // Treat as transient regardless of nested cause code because causes vary + // across runtimes and can be unclassified even for real network faults. if (err instanceof TypeError && err.message === "fetch failed") { - const cause = getErrorCause(err); - if (cause) { - return isTransientNetworkError(cause); - } return true; }