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
- Install OpenClaw v2026.3.12 globally via npm
- Configure Google Chat channel with valid service account
- Start gateway:
systemctl --user start openclaw-gateway.service
- 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"}'
- Observe: 404 Not Found
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 1 — createGatewayPluginRequestHandler 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 2 — setActivePluginRegistry 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:
-
gateway-cli-*.js (2 files): Changed createGatewayPluginRequestHandler to resolve getActivePluginRegistry() ?? _initialRegistry on each request. Also patched shouldEnforcePluginGatewayAuth similarly.
-
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.
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:
createGatewayPluginRequestHandlerdestructures{ registry }from params at construction time and closes over it. During startup,loadOpenClawPluginsis called more than once (config validation, schema building), each call creating a new registry viaactivatePluginRegistry. The Google Chat channel registers its/googlechatroute on the current active registry, but the HTTP handler is still reading from the original captured registry (which has zerohttpRoutes). Result: POST → 404.Issue 2 — Routes lost on registry replacement: Even after fixing Issue 1 (using
getActivePluginRegistry()for live lookup), any runtime call toloadOpenClawPluginsthat misses the plugin cache creates a fresh empty registry and sets it as active viasetActivePluginRegistry. The/googlechathttpRoute 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
systemctl --user start openclaw-gateway.serviceopenclaw channels status --probereports "works" (probe uses internal gateway API, not the HTTP endpoint)Expected behavior
POST
/googlechatshould return 400 (invalid payload) or process the webhook — never 404 while the channel is running.Actual behavior
POST
/googlechatreturns 404. GET/googlechatreturns 200 (Control UI SPA fallback, since plugin route handler returns false due to empty httpRoutes).Debug logging added to
createGatewayPluginRequestHandlerconfirms:The initial (captured) registry has 0 routes. The current active registry has 1 route. They are different objects.
Root cause detail
Issue 1 —
createGatewayPluginRequestHandleratsrc/gateway/server/plugins-http.ts:Issue 2 —
setActivePluginRegistryatsrc/plugins/runtime.ts: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
httpRouteswhen the active registry is replaced. WhensetActivePluginRegistryswaps in a new registry, copy any existinghttpRoutesentries 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:
gateway-cli-*.js (2 files): Changed
createGatewayPluginRequestHandlerto resolvegetActivePluginRegistry() ?? _initialRegistryon each request. Also patchedshouldEnforcePluginGatewayAuthsimilarly.registry-*.js (2 files in dist root): Added a
Object.definePropertyinterceptor on the global registry state'sregistryproperty that copieshttpRoutesfrom old → new on every swap. The_httpRoutePatchAppliedguard 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 --probereports "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 andopenclaw doctor.