From b745bc2441475cd1cf941edf0063bc49f747b8ec Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 20:31:08 +0000 Subject: [PATCH] fix: harden msteams monitor lifecycle edges (#24580) (thanks @chilu18) --- CHANGELOG.md | 1 + .../msteams/src/monitor.lifecycle.test.ts | 18 +++++++++++ extensions/msteams/src/monitor.ts | 31 +++++++++++-------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d6618ff48..10c3eab4527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- MSTeams/gateway lifecycle: keep `startAccount` pending until shutdown/abort to stop false auto-restart loops, fail fast on startup bind errors, and add lifecycle regressions for abort + startup failure behavior. (#24580) Thanks @chilu18. - Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with `204` to avoid persistent `Processing...` states in Synology Chat clients. (#26635) Thanks @memphislee09-source. - Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3. - Synology Chat/reply delivery: resolve webhook usernames to Chat API `user_id` values for outbound chatbot replies, avoiding mismatches between webhook user IDs and `method=chatbot` recipient IDs in multi-account setups. (#23709) Thanks @druide67. diff --git a/extensions/msteams/src/monitor.lifecycle.test.ts b/extensions/msteams/src/monitor.lifecycle.test.ts index abf69b23d0e..f0051620be7 100644 --- a/extensions/msteams/src/monitor.lifecycle.test.ts +++ b/extensions/msteams/src/monitor.lifecycle.test.ts @@ -175,6 +175,24 @@ describe("monitorMSTeamsProvider lifecycle", () => { ); }); + it("returns a shutdown handle when abort signal is not provided", async () => { + const stores = createStores(); + const result = await monitorMSTeamsProvider({ + cfg: createConfig(0), + runtime: createRuntime(), + conversationStore: stores.conversationStore, + pollStore: stores.pollStore, + }); + + expect(result).toEqual( + expect.objectContaining({ + shutdown: expect.any(Function), + }), + ); + + await expect(result.shutdown()).resolves.toBeUndefined(); + }); + it("rejects startup when webhook port is already in use", async () => { expressControl.mode.value = "error"; await expect( diff --git a/extensions/msteams/src/monitor.ts b/extensions/msteams/src/monitor.ts index eab22a890eb..3dae8997f47 100644 --- a/extensions/msteams/src/monitor.ts +++ b/extensions/msteams/src/monitor.ts @@ -277,7 +277,6 @@ export async function monitorMSTeamsProvider( const httpServer = expressApp.listen(port); await new Promise((resolve, reject) => { const onListening = () => { - httpServer.off("error", onError); log.info(`msteams provider started on port ${port}`); resolve(); }; @@ -306,24 +305,30 @@ export async function monitorMSTeamsProvider( }); }; - // Handle abort signal - const onAbort = () => { - void shutdown(); - }; - if (opts.abortSignal) { - if (opts.abortSignal.aborted) { - onAbort(); - } else { - opts.abortSignal.addEventListener("abort", onAbort, { once: true }); - } + // Some direct callers may invoke monitor without lifecycle wiring. + // Return immediately so they can call shutdown explicitly. + if (!opts.abortSignal) { + return { app: expressApp, shutdown }; } - // Keep this task alive until shutdown/close so gateway runtime does not treat startup as exit. - await new Promise((resolve) => { + const closePromise = new Promise((resolve) => { httpServer.once("close", () => { resolve(); }); }); + + // Handle abort signal + const onAbort = () => { + void shutdown(); + }; + if (opts.abortSignal.aborted) { + onAbort(); + } else { + opts.abortSignal.addEventListener("abort", onAbort, { once: true }); + } + + // Keep this task alive until shutdown/close so gateway runtime does not treat startup as exit. + await closePromise; opts.abortSignal?.removeEventListener("abort", onAbort); return { app: expressApp, shutdown };