Skip to content

[Bug]: Webhook routes (Google Chat, LINE, etc.) return 404 — httpRoutes lost on plugin registry swap (v2026.3.12) #45445

@George-Claw

Description

@George-Claw

Bug type

Regression (worked before, now fails)

Summary

Google Chat webhook endpoint (POST /googlechat) returns 404 despite the channel showing as running/configured. This is caused by two compounding issues in how the plugin registry is managed.

Issue 1 — Stale registry capture: createGatewayPluginRequestHandler destructures { registry } from params at construction time and closes over it. During startup, loadOpenClawPlugins is called more than once (config validation, schema building), each call creating a new registry via activatePluginRegistry. The Google Chat channel registers its /googlechat route on the current active registry, but the HTTP handler is still reading from the original captured registry (which has zero httpRoutes). Result: POST → 404.

Issue 2 — Routes lost on registry replacement: Even after fixing Issue 1 (using getActivePluginRegistry() for live lookup), any runtime call to loadOpenClawPlugins that misses the plugin cache creates a fresh empty registry and sets it as active via setActivePluginRegistry. The /googlechat httpRoute was on the previous registry and is not carried forward. The channel does not re-register its route. Result: route works briefly after startup, then 404 again after the next registry swap (triggered by config schema lookups, channel status probes, etc.).

This affects all webhook-based channels (Google Chat, LINE per #34631, BlueBubbles, etc.).

Steps to reproduce

  1. Install OpenClaw v2026.3.12 globally via npm
  2. Configure Google Chat channel with valid service account
  3. Start gateway: systemctl --user start openclaw-gateway.service
  4. Wait 10 seconds, then test:
curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST http://127.0.0.1:18789/googlechat \
  -H "Content-Type: application/json" -d '{"type":"MESSAGE"}'
  1. Observe: 404 Not Found
  2. openclaw channels status --probe reports "works" (probe uses internal gateway API, not the HTTP endpoint)

Expected behavior

POST /googlechat should return 400 (invalid payload) or process the webhook — never 404 while the channel is running.

Actual behavior

POST /googlechat returns 404. GET /googlechat returns 200 (Control UI SPA fallback, since plugin route handler returns false due to empty httpRoutes).

Debug logging added to createGatewayPluginRequestHandler confirms:

initRoutes=0 activeRoutes=1 finalRoutes=1 sameObj=false

The initial (captured) registry has 0 routes. The current active registry has 1 route. They are different objects.

Root cause detail

Issue 1createGatewayPluginRequestHandler at src/gateway/server/plugins-http.ts:

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

Issue 2setActivePluginRegistry at src/plugins/runtime.ts:

function setActivePluginRegistry(registry, cacheKey) {
    state.registry = registry;  // replaces the object; old httpRoutes are orphaned

The global state (globalThis[Symbol.for("openclaw.pluginRegistryState")]) is shared correctly across all bundled chunks, so this is not a dual-instance problem. The issue is purely that (1) the handler captures a stale reference, and (2) dynamically registered httpRoutes are not carried forward when the registry object is replaced.

Suggested fix

For Issue 1: Use getActivePluginRegistry() (already imported in gateway-cli files) as a live lookup in the handler instead of the captured reference. This is the approach in PR #45150, though the Greptile review noted the server.impl.ts wiring was incomplete.

For Issue 2: Carry forward httpRoutes when the active registry is replaced. When setActivePluginRegistry swaps in a new registry, copy any existing httpRoutes entries from the old registry to the new one (deduped by path + match type). This ensures dynamically registered webhook routes survive registry swaps without requiring channels to re-register.

Workaround

We applied a local patch to the dist files covering both issues:

  1. gateway-cli-*.js (2 files): Changed createGatewayPluginRequestHandler to resolve getActivePluginRegistry() ?? _initialRegistry on each request. Also patched shouldEnforcePluginGatewayAuth similarly.

  2. registry-*.js (2 files in dist root): Added a Object.defineProperty interceptor on the global registry state's registry property that copies httpRoutes from old → new on every swap. The _httpRoutePatchApplied guard ensures it installs once despite multiple modules reading the same global.

Both patches are needed. Issue 1 alone fixes initial startup but routes are lost on the first runtime registry swap. Issue 2 alone doesn't help because the handler still reads the captured stale reference.

Related issues

OpenClaw version

2026.3.12

Operating system

Ubuntu 24.04 (Linux 6.14.0), Node.js v24.14.0

Install method

npm global

Additional information

openclaw channels status --probe reports "works" even when the webhook returns 404, because the probe uses the internal gateway WebSocket API rather than the HTTP endpoint. This makes the failure invisible to standard health checks and openclaw doctor.

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