From a4c96fb102147f31d1d5b3d4f10414ba37532b50 Mon Sep 17 00:00:00 2001 From: shin-core <153108882+shin-core@users.noreply.github.com> Date: Sat, 18 Jul 2026 05:14:38 +1000 Subject: [PATCH] fix(miner): fail closed when a chat-action handler throws dispatchChatAction wrapped its paramsValidator call in a try/catch ("fail closed") but left the next line, await registered.handler(request), unguarded -- a throwing handler became an unhandled rejection instead of the module's typed { ok, status } contract every other failure path returns. Wrap the handler call in try/catch, returning { ok: false, status: "handler_error", action, error } on throw, mirroring the paramsValidator catch above. The distinct handler_error status lets a caller tell an execution failure from a validation failure. Closes #6989 --- .../loopover-miner/lib/chat-action-dispatch.js | 10 +++++++++- test/unit/miner-chat-action-dispatch.test.ts | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/loopover-miner/lib/chat-action-dispatch.js b/packages/loopover-miner/lib/chat-action-dispatch.js index 11b44bf25..1faaca4db 100644 --- a/packages/loopover-miner/lib/chat-action-dispatch.js +++ b/packages/loopover-miner/lib/chat-action-dispatch.js @@ -71,7 +71,15 @@ export async function dispatchChatAction(request, options = {}) { return { ok: false, status: "invalid_params", action }; } - const result = await registered.handler(request); + let result; + try { + result = await registered.handler(request); + } catch (error) { + // A handler that throws fails closed with the module's typed result shape (#6989), consistent with the + // paramsValidator catch above -- a distinct "handler_error" status lets a caller tell an execution + // failure apart from a params-validation failure. + return { ok: false, status: "handler_error", action, error: error instanceof Error ? error.message : String(error) }; + } return { ok: true, status: "dispatched", action, result }; } diff --git a/test/unit/miner-chat-action-dispatch.test.ts b/test/unit/miner-chat-action-dispatch.test.ts index f8df273c1..daafef0bc 100644 --- a/test/unit/miner-chat-action-dispatch.test.ts +++ b/test/unit/miner-chat-action-dispatch.test.ts @@ -147,4 +147,21 @@ describe("dispatchChatAction (#6519)", () => { const result = await dispatchChatAction({ action: "demo", params: {} }, { env: enabledEnv, registry }); expect(result).toEqual({ ok: false, status: "invalid_params", action: "demo", error: "boom" }); }); + + it("fails closed with handler_error when the handler throws, not an unhandled rejection (#6989)", async () => { + const registry = registryWith("demo", () => true, () => { + throw new Error("network down"); + }); + const result = await dispatchChatAction({ action: "demo", params: {} }, { env: enabledEnv, registry }); + // Distinct from invalid_params so a caller can tell an execution failure from a validation failure. + expect(result).toEqual({ ok: false, status: "handler_error", action: "demo", error: "network down" }); + }); + + it("stringifies a non-Error thrown by the handler (#6989)", async () => { + const registry = registryWith("demo", () => true, () => { + throw "kaboom"; + }); + const result = await dispatchChatAction({ action: "demo", params: {} }, { env: enabledEnv, registry }); + expect(result).toEqual({ ok: false, status: "handler_error", action: "demo", error: "kaboom" }); + }); });