fix(slack): share HTTP route registry across module loads

Fixes #67955, #46245, #46246.

Co-authored-by: Axel <axel@kaleidoscope.studio>

Co-authored-by: Cesar Arevalo <cesar@cesararevalo.com>
This commit is contained in:
Peter Steinberger
2026-04-24 22:34:13 +01:00
parent 11804a484d
commit 55318df83f
3 changed files with 57 additions and 5 deletions

View File

@@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai
- Codex harness/models: keep legacy `codex/*` harness shorthand out of model picker and `/models` choice surfaces while migrating primary legacy refs to canonical `openai/*` plus explicit Codex harness config. (#71193) Thanks @vincentkoc.
- Plugins/runtime deps: respect explicit plugin and channel disablement when repairing bundled runtime dependencies, so doctor and health checks no longer install deps for disabled configured channels.
- WhatsApp/plugins: support an explicit opt-in for inbound `message_received` hooks with canonical channel, conversation, session, and sender fields. Thanks @vincentkoc.
- Slack/HTTP: keep webhook handlers in a process-global registry so HTTP mode survives plugin-loader/native-import splits and `/slack/events/<account>` no longer returns 404 after logging as active. Fixes #67955, #46245, and #46246. Thanks @chrisabad and @cesararevalo.
- Diagnostics: harden tool and model diagnostic events against hostile errors, blocking listeners, and unsafe stability reason fields. Thanks @vincentkoc.
- Plugins/onboarding: record local plugin install source metadata without duplicating raw absolute local paths in persisted `plugins.installs`, while preserving linked load-path cleanup. (#70970) Thanks @vincentkoc.
- Browser/tool: tell agents not to pass per-call `timeoutMs` on existing-session type, evaluate, and other Chrome MCP actions that reject timeout overrides.

View File

@@ -85,4 +85,43 @@ describe("registerSlackHttpHandler", () => {
'slack: webhook path /slack/events already registered for account "duplicate"',
);
});
it("preserves registered handlers across module reloads", async () => {
const handler = vi.fn();
unregisters.push(
registerSlackHttpHandler({
path: "/slack/events/reload",
handler,
}),
);
vi.resetModules();
const reloadedRegistry = await import("./registry.js");
const req = { url: "/slack/events/reload" } as IncomingMessage;
const res = {} as ServerResponse;
const handled = await reloadedRegistry.handleSlackHttpRequest(req, res);
expect(handled).toBe(true);
expect(handler).toHaveBeenCalledWith(req, res);
});
it("recreates the shared registry if the global slot is corrupted", async () => {
const globalStore = globalThis as Record<PropertyKey, unknown>;
globalStore[Symbol.for("openclaw.slack.httpRoutes.v1")] = {};
const handler = vi.fn();
unregisters.push(
registerSlackHttpHandler({
path: "/slack/events/recovered",
handler,
}),
);
const req = { url: "/slack/events/recovered" } as IncomingMessage;
const res = {} as ServerResponse;
const handled = await handleSlackHttpRequest(req, res);
expect(handled).toBe(true);
expect(handler).toHaveBeenCalledWith(req, res);
});
});

View File

@@ -15,18 +15,30 @@ type RegisterSlackHttpHandlerArgs = {
accountId?: string;
};
const slackHttpRoutes = new Map<string, SlackHttpRequestHandler>();
const SLACK_HTTP_ROUTES_GLOBAL_KEY = Symbol.for("openclaw.slack.httpRoutes.v1");
function getSlackHttpRoutes(): Map<string, SlackHttpRequestHandler> {
const globalStore = globalThis as Record<PropertyKey, unknown>;
const existing = globalStore[SLACK_HTTP_ROUTES_GLOBAL_KEY];
if (existing instanceof Map) {
return existing as Map<string, SlackHttpRequestHandler>;
}
const routes = new Map<string, SlackHttpRequestHandler>();
globalStore[SLACK_HTTP_ROUTES_GLOBAL_KEY] = routes;
return routes;
}
export function registerSlackHttpHandler(params: RegisterSlackHttpHandlerArgs): () => void {
const normalizedPath = normalizeSlackWebhookPath(params.path);
if (slackHttpRoutes.has(normalizedPath)) {
const routes = getSlackHttpRoutes();
if (routes.has(normalizedPath)) {
const suffix = params.accountId ? ` for account "${params.accountId}"` : "";
params.log?.(`slack: webhook path ${normalizedPath} already registered${suffix}`);
return () => {};
}
slackHttpRoutes.set(normalizedPath, params.handler);
routes.set(normalizedPath, params.handler);
return () => {
slackHttpRoutes.delete(normalizedPath);
getSlackHttpRoutes().delete(normalizedPath);
};
}
@@ -35,7 +47,7 @@ export async function handleSlackHttpRequest(
res: ServerResponse,
): Promise<boolean> {
const url = new URL(req.url ?? "/", "http://localhost");
const handler = slackHttpRoutes.get(url.pathname);
const handler = getSlackHttpRoutes().get(url.pathname);
if (!handler) {
return false;
}