openclaw - 💡(How to fix) Fix fix(slack): dual-loader module duplication causes webhook 404s [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#69770Fetched 2026-04-22 07:48:28
View on GitHub
Comments
1
Participants
2
Timeline
1
Reactions
0
Author
Timeline (top)
commented ×1

Error Message

  • All inbound Slack messages silently dropped (no error logged — the handler just returns false)

Root Cause

The dual-loader pattern (loadBundledEntryExportSync using jiti vs native ESM import()) creates two module instances of extensions/slack/src/http/registry.ts. Any module-scoped mutable state (like the slackHttpRoutes Map) becomes silently duplicated.

This is the same class of issue documented in the codebase as "dual-loader diagnostic pattern" — the module is registered in one instance but unreachable from the other.

Code Example

const GLOBAL_KEY = "__openclaw_slack_http_routes__" as const;
const _global = globalThis as Record<string, unknown>;
if (!_global[GLOBAL_KEY]) {
  _global[GLOBAL_KEY] = new Map<string, SlackHttpRequestHandler>();
}
const slackHttpRoutes = _global[GLOBAL_KEY] as Map<string, SlackHttpRequestHandler>;
RAW_BUFFERClick to expand / collapse

Problem

The Slack plugin's HTTP handler registry (slackHttpRoutes Map in extensions/slack/src/http/registry.ts) is module-scoped. The gateway loads this module twice through different mechanisms:

  1. jiti (synchronous) — via loadBundledEntryExportSync during plugin registration (registerSlackPluginHttpRoutesregisterHttpRoute handler closure → handleSlackHttpRequest)
  2. Native ESM import() — via the Bolt monitor at runtime (monitorSlackProviderregisterSlackHttpHandler)

Because jiti and native ESM maintain separate module caches, two independent slackHttpRoutes Map instances are created. The Bolt monitor registers its handlers into one Map, but when inbound webhooks arrive, the HTTP route handler reads from the other (empty) Map. Result: all Slack webhook POSTs return 404.

Impact

  • All inbound Slack messages silently dropped (no error logged — the handler just returns false)
  • Outbound Slack messages (agent → Slack) still work since they go through the Bolt client directly
  • The [slack] http mode listening at /slack/events/... log line appears normally, masking the issue

Root Cause

The dual-loader pattern (loadBundledEntryExportSync using jiti vs native ESM import()) creates two module instances of extensions/slack/src/http/registry.ts. Any module-scoped mutable state (like the slackHttpRoutes Map) becomes silently duplicated.

This is the same class of issue documented in the codebase as "dual-loader diagnostic pattern" — the module is registered in one instance but unreachable from the other.

Suggested Fix

Back the slackHttpRoutes Map with a globalThis singleton so both module instances share the same registry:

const GLOBAL_KEY = "__openclaw_slack_http_routes__" as const;
const _global = globalThis as Record<string, unknown>;
if (!_global[GLOBAL_KEY]) {
  _global[GLOBAL_KEY] = new Map<string, SlackHttpRequestHandler>();
}
const slackHttpRoutes = _global[GLOBAL_KEY] as Map<string, SlackHttpRequestHandler>;

A more structural fix would be to ensure the registry module is only ever loaded through one mechanism (either always jiti or always native ESM), but the globalThis approach is safe and minimal.

Reproduction

  1. Start the gateway
  2. curl -X POST http://127.0.0.1:18790/slack/events/kaleidoscope -H "Content-Type: application/json" -d '{}'
  3. Observe 404 (should be 401 from Bolt signature verification)

extent analysis

TL;DR

The most likely fix is to back the slackHttpRoutes Map with a globalThis singleton to share the same registry across module instances.

Guidance

  • Identify the slackHttpRoutes Map in extensions/slack/src/http/registry.ts and replace it with the suggested globalThis singleton implementation to ensure both module instances share the same registry.
  • Verify that the fix works by reproducing the issue using the provided steps and checking that the 404 error is resolved.
  • Consider a more structural fix to ensure the registry module is only loaded through one mechanism, but the globalThis approach is a safe and minimal solution.
  • Test the fix with different scenarios, such as multiple inbound Slack messages, to ensure the issue is fully resolved.

Example

const GLOBAL_KEY = "__openclaw_slack_http_routes__" as const;
const _global = globalThis as Record<string, unknown>;
if (!_global[GLOBAL_KEY]) {
  _global[GLOBAL_KEY] = new Map<string, SlackHttpRequestHandler>();
}
const slackHttpRoutes = _global[GLOBAL_KEY] as Map<string, SlackHttpRequestHandler>;

Notes

This fix assumes that the globalThis approach is sufficient to resolve the issue. However, a more structural fix may be necessary to ensure the registry module is only loaded through one mechanism.

Recommendation

Apply the workaround by backing the slackHttpRoutes Map with a globalThis singleton, as it is a safe and minimal solution that can be implemented quickly to resolve the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix fix(slack): dual-loader module duplication causes webhook 404s [1 comments, 2 participants]