Skip to content

Plugin HTTP route dispatch uses stale registry - webhook POST returns 404 #48734

@reidmcgill

Description

@reidmcgill

Bug Description

BlueBubbles (and likely other channel plugins) webhook HTTP routes return 404 despite being correctly registered during gateway startup. The route IS registered and appears in logs (BlueBubbles webhook listening on /bluebubbles-webhook), but POST requests fall through to the 404 fallback.

Root Cause

loadOpenClawPlugins() is called approximately 9 times during gateway startup. Each invocation creates a new plugin registry object and sets it as the global active registry via globalThis[Symbol.for("openclaw.pluginRegistryState")].

The HTTP request handler is created with createGatewayPluginRequestHandler({ registry: params.pluginRegistry, ... }), which captures a closure reference to the registry from loadGatewayPlugins() — one of the earliest calls.

Later, when startChannels() runs, channel plugins (e.g., BlueBubbles) call registerPluginHttpRoute(), which calls requireActivePluginRegistry() — returning the latest active registry (a different object, version 9). The route is added to this new registry.

At request time, the HTTP handler checks registry.httpRoutes on the stale closure reference (version 1, 0 routes) → returns false → falls through to 404.

Confirmed by debug instrumentation:

  • At handler creation: httpRoutes=0, registry-id=0.218...
  • After startChannels(): handler-registry routes=0, active-registry routes=0, same=false, version=9
  • The same=false confirms the two registries are different objects despite both using Symbol.for("openclaw.pluginRegistryState") on globalThis.

Expected Behavior

Plugin HTTP routes registered during channel startup should be dispatchable by the gateway HTTP handler.

Workaround

Patch createGatewayPluginRequestHandler and shouldEnforcePluginGatewayAuth in the active gateway-cli-*.js to read the global active registry at request time:

// Instead of:
const { registry, log } = params;

// Use:
const _getRegistry = () => {
  const _s = globalThis[Symbol.for("openclaw.pluginRegistryState")];
  return _s?.registry ?? params.registry;
};
// Then: const registry = _getRegistry(); at request time

Environment

  • OpenClaw 2026.3.13 (61d171a)
  • macOS 26.3.0 (arm64)
  • Node.js v25.8.1
  • BlueBubbles 1.9.9

Suggested Fix

Either:

  1. Make createGatewayPluginRequestHandler use getActivePluginRegistry() at request time instead of the captured closure reference
  2. Ensure loadOpenClawPlugins() doesn't create redundant registries during startup (reduce the 9 calls)
  3. Pass the registry object explicitly to channel plugins during startChannel() so they register on the same object the HTTP handler uses

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions