-
-
Notifications
You must be signed in to change notification settings - Fork 69.6k
[Bug]: Webhook routes (Google Chat, LINE, etc.) return 404 — httpRoutes lost on plugin registry swap (v2026.3.12) #45445
Description
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 --probereports "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 referenceIssue 2 — setActivePluginRegistry at src/plugins/runtime.ts:
function setActivePluginRegistry(registry, cacheKey) {
state.registry = registry; // replaces the object; old httpRoutes are orphanedThe 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
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
- [Bug]: Title: LINE webhook route intermittently returns 404 until gateway restart (2026.3.2) #34631 — LINE webhook 404, same root cause (open)
- fix: resolve plugin registry via getter injection to fix LINE webhook 404 #45150 — PR for getter injection fix (addresses Issue 1 only, incomplete per Greptile review)
- fix(line): use getRegistry callback in plugin HTTP handler to prevent stale-route 404 #40224 — Earlier PR attempt for the same fix
- [Bug]: Google Chat agents don't respond to messages in v2026.3.2 (regression from v2026.2.26) #38369 — Google Chat not responding in v2026.3.2 (different root cause, fixed)
- [2026.3.1 regression] Control UI handler runs before plugin HTTP routes — plugin routes unreachable #31766 — Control UI handler shadows plugin routes (fixed in 2026.3.7+)
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.