From d4021f4b92ddb316b77964b344a250c39cb7cd9a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 5 Mar 2026 23:23:24 -0500 Subject: [PATCH] Plugins: clarify registerHttpHandler migration errors (#36794) * Changelog: note plugin HTTP route migration diagnostics * Tests: cover registerHttpHandler migration diagnostics * Plugins: clarify registerHttpHandler migration errors * Tests: cover registerHttpHandler diagnostic edge cases * Plugins: tighten registerHttpHandler migration hint --- CHANGELOG.md | 3 +- src/plugins/loader.test.ts | 68 ++++++++++++++++++++++++++++++++++++++ src/plugins/loader.ts | 11 ++++-- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd7351d59d6..4326784db41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,12 +164,11 @@ Docs: https://docs.openclaw.ai - LINE cleanup/test follow-ups: fold cleanup/test learnings into the synthesis review path while keeping runtime changes focused on regression fixes. (from #17630, #17289) Thanks @Clawborn and @davidahmann. - Mattermost/interactive buttons: add interactive button send/callback support with directory-based channel/user target resolution, and harden callbacks via account-scoped HMAC verification plus sender-scoped DM routing. (#19957) thanks @tonydehnke. - Feishu/groupPolicy legacy alias compatibility: treat legacy `groupPolicy: "allowall"` as `open` in both schema parsing and runtime policy checks so intended open-group configs no longer silently drop group messages when `groupAllowFrom` is empty. (from #36358) Thanks @Sid-Qin. - - Mattermost/plugin SDK import policy: replace remaining monolithic `openclaw/plugin-sdk` imports in Mattermost mention-gating paths/tests with scoped subpaths (`openclaw/plugin-sdk/compat` and `openclaw/plugin-sdk/mattermost`) so `pnpm check` passes `lint:plugins:no-monolithic-plugin-sdk-entry-imports` on baseline. (#36480) Thanks @Takhoffman. - Telegram/polls: add Telegram poll action support to channel action discovery and tool/CLI poll flows, with multi-account discoverability gated to accounts that can actually execute polls (`sendMessage` + `poll`). (#36547) thanks @gumadeiras. - - Agents/failover cooldown classification: stop treating generic `cooling down` text as provider `rate_limit` so healthy models no longer show false global cooldown/rate-limit warnings while explicit `model_cooldown` markers still trigger failover. (#32972) thanks @stakeswky. - Agents/failover service-unavailable handling: stop treating bare proxy/CDN `service unavailable` errors as provider overload while keeping them retryable via the timeout/failover path, so transient outages no longer show false rate-limit warnings or block fallback. (#36646) thanks @jnMetaCode. +- Plugins/HTTP route migration diagnostics: rewrite legacy `api.registerHttpHandler(...)` loader failures into actionable migration guidance so doctor/plugin diagnostics point operators to `api.registerHttpRoute(...)` or `registerPluginHttpRoute(...)`. (#36794) Thanks @vincentkoc - Doctor/Heartbeat upgrade diagnostics: warn when heartbeat delivery is configured with an implicit `directPolicy` so upgrades pin direct/DM behavior explicitly instead of relying on the current default. (#36789) Thanks @vincentkoc. - Agents/current-time UTC anchor: append a machine-readable UTC suffix alongside local `Current time:` lines in shared cron-style prompt contexts so agents can compare UTC-stamped workspace timestamps without doing timezone math. (#32423) thanks @jriff. - TUI/webchat command-owner scope alignment: treat internal-channel gateway sessions with `operator.admin` as owner-authorized in command auth, restoring cron/gateway/connector tool access for affected TUI/webchat sessions while keeping external channels on identity-based owner checks. (from #35666, #35673, #35704) Thanks @Naylenv, @Octane0411, and @Sid-Qin. diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 5bebad861bb..cdd23edbfa8 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -191,6 +191,15 @@ function createWarningLogger(warnings: string[]) { }; } +function createErrorLogger(errors: string[]) { + return { + info: () => {}, + warn: () => {}, + error: (msg: string) => errors.push(msg), + debug: () => {}, + }; +} + function createEscapingEntryFixture(params: { id: string; sourceBody: string }) { const pluginDir = makeTempDir(); const outsideDir = makeTempDir(); @@ -574,6 +583,65 @@ describe("loadOpenClawPlugins", () => { expect(httpPlugin?.httpRoutes).toBe(1); }); + it("rewrites removed registerHttpHandler failures into migration diagnostics", () => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id: "http-handler-legacy", + filename: "http-handler-legacy.cjs", + body: `module.exports = { id: "http-handler-legacy", register(api) { + api.registerHttpHandler({ path: "/legacy", handler: async () => true }); +} };`, + }); + + const errors: string[] = []; + const registry = loadRegistryFromSinglePlugin({ + plugin, + pluginConfig: { + allow: ["http-handler-legacy"], + }, + options: { + logger: createErrorLogger(errors), + }, + }); + + const loaded = registry.plugins.find((entry) => entry.id === "http-handler-legacy"); + expect(loaded?.status).toBe("error"); + expect(loaded?.error).toContain("api.registerHttpHandler(...) was removed"); + expect(loaded?.error).toContain("api.registerHttpRoute(...)"); + expect(loaded?.error).toContain("registerPluginHttpRoute(...)"); + expect( + registry.diagnostics.some((diag) => + String(diag.message).includes("api.registerHttpHandler(...) was removed"), + ), + ).toBe(true); + expect(errors.some((entry) => entry.includes("api.registerHttpHandler(...) was removed"))).toBe( + true, + ); + }); + + it("does not rewrite unrelated registerHttpHandler helper failures", () => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id: "http-handler-local-helper", + filename: "http-handler-local-helper.cjs", + body: `module.exports = { id: "http-handler-local-helper", register() { + const registerHttpHandler = undefined; + registerHttpHandler(); +} };`, + }); + + const registry = loadRegistryFromSinglePlugin({ + plugin, + pluginConfig: { + allow: ["http-handler-local-helper"], + }, + }); + + const loaded = registry.plugins.find((entry) => entry.id === "http-handler-local-helper"); + expect(loaded?.status).toBe("error"); + expect(loaded?.error).not.toContain("api.registerHttpHandler(...) was removed"); + }); + it("rejects plugin http routes missing explicit auth", () => { useNoBundledPlugins(); const plugin = writePlugin({ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index c70bfc09251..482eeead5de 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -297,16 +297,21 @@ function recordPluginError(params: { diagnosticMessagePrefix: string; }) { const errorText = String(params.error); - params.logger.error(`${params.logPrefix}${errorText}`); + const deprecatedApiHint = + errorText.includes("api.registerHttpHandler") && errorText.includes("is not a function") + ? "deprecated api.registerHttpHandler(...) was removed; use api.registerHttpRoute(...) for plugin-owned routes or registerPluginHttpRoute(...) for dynamic lifecycle routes" + : null; + const displayError = deprecatedApiHint ? `${deprecatedApiHint} (${errorText})` : errorText; + params.logger.error(`${params.logPrefix}${displayError}`); params.record.status = "error"; - params.record.error = errorText; + params.record.error = displayError; params.registry.plugins.push(params.record); params.seenIds.set(params.pluginId, params.origin); params.registry.diagnostics.push({ level: "error", pluginId: params.record.id, source: params.record.source, - message: `${params.diagnosticMessagePrefix}${errorText}`, + message: `${params.diagnosticMessagePrefix}${displayError}`, }); }