Skip to content

Commit a69f619

Browse files
authored
fix(gateway): pin plugin webhook route registry (openclaw#47902)
1 parent 99a4594 commit a69f619

File tree

10 files changed

+511
-223
lines changed

10 files changed

+511
-223
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai
4646
- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc.
4747
- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc.
4848
- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc.
49+
- Gateway/plugins: pin runtime webhook routes to the gateway startup registry so channel webhooks keep working across plugin-registry churn, and make plugin auth + dispatch resolve routes from the same live HTTP-route registry. Fixes #46924 and #47041.
4950
- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc.
5051
- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc.
5152
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.

src/gateway/server-close.ts

Lines changed: 97 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function createGatewayCloseHandler(params: {
1111
tailscaleCleanup: (() => Promise<void>) | null;
1212
canvasHost: CanvasHostHandler | null;
1313
canvasHostServer: CanvasHostServer | null;
14+
releasePluginRouteRegistry?: (() => void) | null;
1415
stopChannel: (name: ChannelId, accountId?: string) => Promise<void>;
1516
pluginServices: PluginServicesHandle | null;
1617
cron: { stop: () => void };
@@ -33,106 +34,114 @@ export function createGatewayCloseHandler(params: {
3334
httpServers?: HttpServer[];
3435
}) {
3536
return async (opts?: { reason?: string; restartExpectedMs?: number | null }) => {
36-
const reasonRaw = typeof opts?.reason === "string" ? opts.reason.trim() : "";
37-
const reason = reasonRaw || "gateway stopping";
38-
const restartExpectedMs =
39-
typeof opts?.restartExpectedMs === "number" && Number.isFinite(opts.restartExpectedMs)
40-
? Math.max(0, Math.floor(opts.restartExpectedMs))
41-
: null;
42-
if (params.bonjourStop) {
43-
try {
44-
await params.bonjourStop();
45-
} catch {
46-
/* ignore */
37+
try {
38+
const reasonRaw = typeof opts?.reason === "string" ? opts.reason.trim() : "";
39+
const reason = reasonRaw || "gateway stopping";
40+
const restartExpectedMs =
41+
typeof opts?.restartExpectedMs === "number" && Number.isFinite(opts.restartExpectedMs)
42+
? Math.max(0, Math.floor(opts.restartExpectedMs))
43+
: null;
44+
if (params.bonjourStop) {
45+
try {
46+
await params.bonjourStop();
47+
} catch {
48+
/* ignore */
49+
}
4750
}
48-
}
49-
if (params.tailscaleCleanup) {
50-
await params.tailscaleCleanup();
51-
}
52-
if (params.canvasHost) {
53-
try {
54-
await params.canvasHost.close();
55-
} catch {
56-
/* ignore */
51+
if (params.tailscaleCleanup) {
52+
await params.tailscaleCleanup();
5753
}
58-
}
59-
if (params.canvasHostServer) {
60-
try {
61-
await params.canvasHostServer.close();
62-
} catch {
63-
/* ignore */
54+
if (params.canvasHost) {
55+
try {
56+
await params.canvasHost.close();
57+
} catch {
58+
/* ignore */
59+
}
6460
}
65-
}
66-
for (const plugin of listChannelPlugins()) {
67-
await params.stopChannel(plugin.id);
68-
}
69-
if (params.pluginServices) {
70-
await params.pluginServices.stop().catch(() => {});
71-
}
72-
await stopGmailWatcher();
73-
params.cron.stop();
74-
params.heartbeatRunner.stop();
75-
try {
76-
params.updateCheckStop?.();
77-
} catch {
78-
/* ignore */
79-
}
80-
for (const timer of params.nodePresenceTimers.values()) {
81-
clearInterval(timer);
82-
}
83-
params.nodePresenceTimers.clear();
84-
params.broadcast("shutdown", {
85-
reason,
86-
restartExpectedMs,
87-
});
88-
clearInterval(params.tickInterval);
89-
clearInterval(params.healthInterval);
90-
clearInterval(params.dedupeCleanup);
91-
if (params.mediaCleanup) {
92-
clearInterval(params.mediaCleanup);
93-
}
94-
if (params.agentUnsub) {
95-
try {
96-
params.agentUnsub();
97-
} catch {
98-
/* ignore */
61+
if (params.canvasHostServer) {
62+
try {
63+
await params.canvasHostServer.close();
64+
} catch {
65+
/* ignore */
66+
}
9967
}
100-
}
101-
if (params.heartbeatUnsub) {
68+
for (const plugin of listChannelPlugins()) {
69+
await params.stopChannel(plugin.id);
70+
}
71+
if (params.pluginServices) {
72+
await params.pluginServices.stop().catch(() => {});
73+
}
74+
await stopGmailWatcher();
75+
params.cron.stop();
76+
params.heartbeatRunner.stop();
10277
try {
103-
params.heartbeatUnsub();
78+
params.updateCheckStop?.();
10479
} catch {
10580
/* ignore */
10681
}
107-
}
108-
params.chatRunState.clear();
109-
for (const c of params.clients) {
82+
for (const timer of params.nodePresenceTimers.values()) {
83+
clearInterval(timer);
84+
}
85+
params.nodePresenceTimers.clear();
86+
params.broadcast("shutdown", {
87+
reason,
88+
restartExpectedMs,
89+
});
90+
clearInterval(params.tickInterval);
91+
clearInterval(params.healthInterval);
92+
clearInterval(params.dedupeCleanup);
93+
if (params.mediaCleanup) {
94+
clearInterval(params.mediaCleanup);
95+
}
96+
if (params.agentUnsub) {
97+
try {
98+
params.agentUnsub();
99+
} catch {
100+
/* ignore */
101+
}
102+
}
103+
if (params.heartbeatUnsub) {
104+
try {
105+
params.heartbeatUnsub();
106+
} catch {
107+
/* ignore */
108+
}
109+
}
110+
params.chatRunState.clear();
111+
for (const c of params.clients) {
112+
try {
113+
c.socket.close(1012, "service restart");
114+
} catch {
115+
/* ignore */
116+
}
117+
}
118+
params.clients.clear();
119+
await params.configReloader.stop().catch(() => {});
120+
if (params.browserControl) {
121+
await params.browserControl.stop().catch(() => {});
122+
}
123+
await new Promise<void>((resolve) => params.wss.close(() => resolve()));
124+
const servers =
125+
params.httpServers && params.httpServers.length > 0
126+
? params.httpServers
127+
: [params.httpServer];
128+
for (const server of servers) {
129+
const httpServer = server as HttpServer & {
130+
closeIdleConnections?: () => void;
131+
};
132+
if (typeof httpServer.closeIdleConnections === "function") {
133+
httpServer.closeIdleConnections();
134+
}
135+
await new Promise<void>((resolve, reject) =>
136+
httpServer.close((err) => (err ? reject(err) : resolve())),
137+
);
138+
}
139+
} finally {
110140
try {
111-
c.socket.close(1012, "service restart");
141+
params.releasePluginRouteRegistry?.();
112142
} catch {
113143
/* ignore */
114144
}
115145
}
116-
params.clients.clear();
117-
await params.configReloader.stop().catch(() => {});
118-
if (params.browserControl) {
119-
await params.browserControl.stop().catch(() => {});
120-
}
121-
await new Promise<void>((resolve) => params.wss.close(() => resolve()));
122-
const servers =
123-
params.httpServers && params.httpServers.length > 0
124-
? params.httpServers
125-
: [params.httpServer];
126-
for (const server of servers) {
127-
const httpServer = server as HttpServer & {
128-
closeIdleConnections?: () => void;
129-
};
130-
if (typeof httpServer.closeIdleConnections === "function") {
131-
httpServer.closeIdleConnections();
132-
}
133-
await new Promise<void>((resolve, reject) =>
134-
httpServer.close((err) => (err ? reject(err) : resolve())),
135-
);
136-
}
137146
};
138147
}

0 commit comments

Comments
 (0)