Skip to content

[Bug]: Plugin HTTP routes invisible to gateway — registry desynchronization in createGatewayPluginRequestHandler #47041

@mmarcus006

Description

@mmarcus006

Bug type

Behavior bug (incorrect output/state without crash)

Summary

createGatewayPluginRequestHandler captures a stale plugin registry reference at HTTP server creation time. Plugin channels (e.g. BlueBubbles) register their webhook HTTP routes into a different registry instance via requireActivePluginRegistry(), making all plugin HTTP routes invisible to the gateway's request handler. POST/PUT to registered webhook paths return 404.

Steps to reproduce

  1. Install OpenClaw v2026.3.13 via Homebrew (npm i -g openclaw).
  2. Configure a BlueBubbles channel with webhookPath: "/bluebubbles-webhook".
  3. Start the gateway: openclaw gateway start.
  4. Confirm logs show: [bluebubbles] [default] BlueBubbles webhook listening on /bluebubbles-webhook.
  5. Confirm channel status is OK: openclaw status --deep → BlueBubbles: OK.
  6. Send a POST request to the webhook:
    curl -s -o /dev/null -w "%{http_code}" -X POST http://127.0.0.1:18789/bluebubbles-webhook \
      -H "Content-Type: application/json" -d '{"type":"new-message","data":{}}'
  7. Observe: 404 Not Found (expected: 401 Unauthorized or 200).
  8. Send a PUT request (should be 405 Method Not Allowed if the route is registered):
    curl -s -o /dev/null -w "%{http_code}" -X PUT http://127.0.0.1:18789/bluebubbles-webhook
  9. Observe: 404 Not Found (expected: 405).

This reproduces on every fresh process start — it is not caused by hot-reload or config editing.

Expected behavior

  • POST to /bluebubbles-webhook without credentials → 401 Unauthorized (route matched, auth failed).
  • PUT to /bluebubbles-webhook405 Method Not Allowed (route matched, wrong method).
  • POST with valid ?password=200 OK (webhook processed).

Actual behavior

  • POST and PUT to /bluebubbles-webhook404 Not Found.
  • Gateway logs confirm BlueBubbles webhook listening on /bluebubbles-webhook — the channel believes it registered the route.
  • Injected diagnostics into createGatewayPluginRequestHandler confirm registry.httpRoutes.length === 0 on every request, while the active plugin registry (via getActivePluginRegistry()) contains the registered routes.

OpenClaw version

2026.3.13

Operating system

macOS 26.3.1 (Tahoe) arm64

Install method

Homebrew (npm i -g openclaw)

Model

anthropic/claude-opus-4-6 (not model-dependent — this is a gateway HTTP routing bug)

Provider / routing chain

N/A — bug is in the gateway HTTP server layer, not the model/provider chain

Config file / key location

~/.openclaw/openclaw.json ; channels.bluebubbles.webhookPath ; plugins.allow includes "bluebubbles"

Additional provider/model setup details

This bug affects any channel plugin that registers HTTP routes via registerPluginHttpRoute / registerWebhookTargetWithPluginRoute. BlueBubbles is the primary affected channel but any plugin using the same pattern would have the same issue.

Logs, screenshots, and evidence

Diagnostic injected into createGatewayPluginRequestHandler (gateway-cli-Ol-vpIk7.js):

[DIAG-PLUGIN] routes=0 routePaths= url=/bluebubbles-webhook
[DIAG] no stage handled method=POST url=/bluebubbles-webhook stageCount=11
       stageNames=hooks,tools-invoke,slack,canvas-auth,a2ui,canvas-http,
       plugin-auth,plugin-http,control-ui-avatar,control-ui-http,gateway-probes

The routes=0 proves the captured registry object has no httpRoutes, while the channel successfully called registerPluginHttpRoute (which pushed routes into a different registry obtained via requireActivePluginRegistry()).

Root cause in code:

In gateway-cli-Ol-vpIk7.js, createGatewayPluginRequestHandler (line ~19542) captures the registry at closure creation time:

function createGatewayPluginRequestHandler(params) {
    const { registry, log } = params;          // ← captured once
    return async (req, res, ...) => {
        if ((registry.httpRoutes ?? []).length === 0) return false;  // ← reads stale ref
        const matchedRoutes = findMatchingPluginHttpRoutes(registry, pathContext);
        // ...
    };
}

But registerPluginHttpRoute (called by channel plugins during startup) uses requireActivePluginRegistry() to find the registry:

function registerPluginHttpRoute(params) {
    const registry = params.registry ?? requireActivePluginRegistry();  // ← different object
    // ...
    routes.push(entry);  // ← pushed to the active registry, not the captured one
}

loadOpenClawPlugins is called multiple times during gateway startup (directly in loadGatewayPlugins and indirectly via loadSchemaWithPlugins for config validation). Each call may produce a different registry object and update the active pointer via setActivePluginRegistry(). The HTTP server is created with one registry, but channel startup happens later when the active registry has already been replaced.

Working patch (applied and verified):

function createGatewayPluginRequestHandler(params) {
    const { registry: _fallbackRegistry, log } = params;
    return async (req, res, providedPathContext, dispatchContext) => {
        const registry = getActivePluginRegistry() ?? _fallbackRegistry;  // ← live lookup
        if ((registry.httpRoutes ?? []).length === 0) return false;
        // ... rest unchanged
    };
}

Same fix applied to shouldEnforcePluginGatewayAuth:

const shouldEnforcePluginGatewayAuth = (pathContext) => {
    return shouldEnforceGatewayAuthForPluginPath(
        getActivePluginRegistry() ?? params.pluginRegistry, pathContext
    );
};

After applying the patch:

  • PUT → 405
  • POST (no auth) → 401

Impact and severity

  • Affected: All users of plugin-based channel webhooks (BlueBubbles confirmed; potentially WhatsApp, Signal, Google Chat, or any plugin using registerWebhookTargetWithPluginRoute).
  • Severity: High — completely blocks inbound message delivery via webhooks.
  • Frequency: 100% reproducible on every fresh gateway start in v2026.3.13.
  • Consequence: Channels appear healthy in openclaw status --deep but cannot receive any inbound webhook messages. Silent message loss with no error surfaced to the user.

Additional information

  • Workaround: Patch gateway-cli-Ol-vpIk7.js as described above (2 edits). The patch is overwritten on openclaw update.
  • Suggested fix: Either (a) have createGatewayPluginRequestHandler read from getActivePluginRegistry() on each request (as in the patch), or (b) have registerPluginHttpRoute accept an explicit registry parameter from the caller (the channel system knows which registry it was started with), or (c) prevent loadOpenClawPlugins from creating new registry objects when called for schema-only purposes.
  • The internal hooks system already handles this pattern correctly — triggerInternalHook uses Symbol.for("openclaw.__openclaw_internal_hook_handlers__") as a process-global singleton, avoiding the same class of desynchronization.
  • Note: gateway-cli-CuZs0RlJ.js also exists as a duplicate bundle but is NOT the one loaded at runtime. The active bundle is gateway-cli-Ol-vpIk7.js (loaded via dynamic import in program-context-KfI9f4T2.js).

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