-
-
Notifications
You must be signed in to change notification settings - Fork 69.3k
BlueBubbles webhook route invisible to gateway handler (registry object mismatch) #45598
Description
Summary
Channel plugins that register webhook routes via registerPluginHttpRoute() (called from registerWebhookTargetWithPluginRoute()) add routes to the global singleton plugin registry, but the gateway's HTTP request handler holds a reference to a different registry object. The result: webhook POST requests return 404 even though the route is registered and the logs say it's listening.
This affects BlueBubbles (and likely any channel plugin using registerWebhookTargetWithPluginRoute). Plugins that register via api.registerHttpRoute() during plugin loading (e.g., twilio-whatsapp) are unaffected because they write to the correct registry.
Version
OpenClaw 2026.3.12 (Homebrew, macOS, Node.js 25.5.0)
Steps to Reproduce
- Configure BlueBubbles channel in openclaw.json with a webhook path
- Start the gateway
- POST to the webhook path (e.g.,
curl -X POST http://localhost:18789/bluebubbles-webhook) - Observe 404 "Not Found"
Root Cause
In createGatewayPluginRequestHandler() (gateway-cli chunk), the registry parameter is captured by closure:
function createGatewayPluginRequestHandler(params) {
const { registry, log } = params;
return async (req, res, ...) => {
// registry is the object from loadOpenClawPlugins()
const matchedRoutes = findMatchingPluginHttpRoutes(registry, pathContext);
};
}Later, when a channel provider starts, registerPluginHttpRoute() is called without a registry parameter, so it falls back to requireActivePluginRegistry():
function registerPluginHttpRoute(params) {
const registry = params.registry ?? requireActivePluginRegistry();
// ...
routes.push(entry);
}requireActivePluginRegistry() returns globalThis[Symbol.for("openclaw.pluginRegistryState")].registry, which is a different object than the one captured by the handler closure. Confirmed with instrumentation:
[bb-fix] handler created: same=false capturedRoutes=1
[bb-fix] req: path=/bluebubbles-webhook live=2 captured=1 same=false
The captured registry has 1 route (twilio-whatsapp, registered during plugin loading). The global registry has 2 routes (twilio-whatsapp + bluebubbles). The handler only checks the captured one.
Workaround
Patch createGatewayPluginRequestHandler in the gateway-cli chunk to read from the live global registry on each request:
function createGatewayPluginRequestHandler(params) {
const { registry, log } = params;
const _RSYM = Symbol.for("openclaw.pluginRegistryState");
const _getGlobalRegistry = () => globalThis[_RSYM]?.registry ?? registry;
return async (req, res, providedPathContext, dispatchContext) => {
const _liveRegistry = _getGlobalRegistry();
if ((_liveRegistry.httpRoutes ?? []).length === 0) return false;
// ... use _liveRegistry instead of registry ...
};
}Also requires NODE_DISABLE_COMPILE_CACHE=1 in the launchd plist environment to prevent Node.js compile cache from serving stale bytecode.
Additional Context
openclaw doctorreports BlueBubbles as healthy- Outbound iMessages work fine
- The health monitor detects "stale-socket" every ~35 minutes (because no inbound webhooks ever arrive) and restarts the provider, which triggers a register/unregister/register cycle but never fixes the underlying registry mismatch
- The
shouldEnforceGatewayAuthForPluginPathfunction at the call site also uses the stale captured registry, though this doesn't directly cause the 404 since BlueBubbles usesauth: "plugin"