fix(feishu): await HTTP server close during monitor cleanup

The Feishu monitor's stopFeishuMonitorState() called server.close()
without awaiting the callback, then immediately cleared the Map
entries. This caused:

1. Memory leak — server objects retained after Map.delete()
2. Port binding issues — ports not fully released before restart
3. Resource leak on repeated gateway restarts

Fix: make stopFeishuMonitorState() and stopFeishuMonitor() async,
await server.close() via Promise wrapper before clearing state.
Update all test afterEach hooks to await the cleanup.

Closes #48183
This commit is contained in:
elliotllliu 2026-03-16 15:56:43 +00:00
parent 1b234b910b
commit f43072f955
5 changed files with 19 additions and 11 deletions

View File

@ -44,8 +44,8 @@ async function waitForStartedAccount(started: string[], accountId: string) {
}
}
afterEach(() => {
stopFeishuMonitor();
afterEach(async () => {
await stopFeishuMonitor();
});
describe("Feishu monitor startup preflight", () => {

View File

@ -132,13 +132,15 @@ export function recordWebhookStatus(
});
}
export function stopFeishuMonitorState(accountId?: string): void {
export async function stopFeishuMonitorState(accountId?: string): Promise<void> {
if (accountId) {
wsClients.delete(accountId);
const server = httpServers.get(accountId);
if (server) {
server.close();
httpServers.delete(accountId);
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
}
botOpenIds.delete(accountId);
botNames.delete(accountId);
@ -146,10 +148,16 @@ export function stopFeishuMonitorState(accountId?: string): void {
}
wsClients.clear();
const closePromises: Promise<void>[] = [];
for (const server of httpServers.values()) {
server.close();
closePromises.push(
new Promise<void>((resolve) => {
server.close(() => resolve());
}),
);
}
httpServers.clear();
await Promise.all(closePromises);
botOpenIds.clear();
botNames.clear();
}

View File

@ -90,6 +90,6 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
await Promise.all(monitorPromises);
}
export function stopFeishuMonitor(accountId?: string): void {
stopFeishuMonitorState(accountId);
export async function stopFeishuMonitor(accountId?: string): Promise<void> {
await stopFeishuMonitorState(accountId);
}

View File

@ -58,8 +58,8 @@ async function postSignedPayload(url: string, payload: Record<string, unknown>)
});
}
afterEach(() => {
stopFeishuMonitor();
afterEach(async () => {
await stopFeishuMonitor();
});
describe("Feishu webhook signed-request e2e", () => {

View File

@ -35,9 +35,9 @@ import {
stopFeishuMonitor,
} from "./monitor.js";
afterEach(() => {
afterEach(async () => {
clearFeishuWebhookRateLimitStateForTest();
stopFeishuMonitor();
await stopFeishuMonitor();
});
describe("Feishu webhook security hardening", () => {