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
This commit is contained in:
Vincent Koc
2026-03-05 23:23:24 -05:00
committed by GitHub
parent e5481ac79f
commit d4021f4b92
3 changed files with 77 additions and 5 deletions

View File

@@ -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.

View File

@@ -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({

View File

@@ -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}`,
});
}