fix: abort telegram getupdates on shutdown (#23950) (thanks @Gkinthecodeland)

This commit is contained in:
Peter Steinberger
2026-03-09 06:03:28 +00:00
parent 6186f620d2
commit 2d5e70f3e7
4 changed files with 36 additions and 23 deletions

View File

@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
- Gateway/restart timeout recovery: exit non-zero when restart-triggered shutdown drains time out so launchd/systemd restart the gateway instead of treating the failed restart as a clean stop. Landed from contributor PR #40380 by @dsantoreis. Thanks @dsantoreis.
- Gateway/config restart guard: validate config before service start/restart and keep post-SIGUSR1 startup failures from crashing the gateway process, reducing invalid-config restart loops and macOS permission loss. Landed from contributor PR #38699 by @lml2468. Thanks @lml2468.
- Gateway/launchd respawn detection: treat `XPC_SERVICE_NAME` as a launchd supervision hint so macOS restarts exit cleanly under launchd instead of attempting detached self-respawn. Landed from contributor PR #20555 by @dimat. Thanks @dimat.
- Telegram/poll restart cleanup: abort the in-flight Telegram API fetch when shutdown or forced polling restarts stop a runner, preventing stale `getUpdates` long polls from colliding with the replacement runner. Landed from contributor PR #23950 by @Gkinthecodeland. Thanks @Gkinthecodeland.
- Cron/owner-only tools: pass trusted isolated cron runs into the embedded agent with owner context so `cron`/`gateway` tooling remains available after the owner-auth hardening narrowed direct-message ownership inference.
## 2026.3.7

View File

@@ -75,27 +75,6 @@ describe("createTelegramBot", () => {
globalThis.fetch = originalFetch;
}
});
it("aborts wrapped client fetch when fetchAbortSignal aborts", async () => {
const originalFetch = globalThis.fetch;
const fetchSpy = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => init?.signal);
const shutdown = new AbortController();
globalThis.fetch = fetchSpy as unknown as typeof fetch;
try {
createTelegramBot({ token: "tok", fetchAbortSignal: shutdown.signal });
const clientFetch = (botCtorSpy.mock.calls[0]?.[1] as { client?: { fetch?: unknown } })
?.client?.fetch as ((input: RequestInfo | URL, init?: RequestInit) => Promise<unknown>);
expect(clientFetch).toBeTypeOf("function");
const observedSignal = (await clientFetch("https://example.test")) as AbortSignal;
expect(observedSignal).toBeInstanceOf(AbortSignal);
expect(observedSignal.aborted).toBe(false);
shutdown.abort(new Error("shutdown"));
expect(observedSignal.aborted).toBe(true);
} finally {
globalThis.fetch = originalFetch;
}
});
it("applies global and per-account timeoutSeconds", () => {
loadConfig.mockReturnValue({
channels: {

View File

@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from "vitest";
import { botCtorSpy } from "./bot.create-telegram-bot.test-harness.js";
import { createTelegramBot } from "./bot.js";
describe("createTelegramBot fetch abort", () => {
it("aborts wrapped client fetch when fetchAbortSignal aborts", async () => {
const originalFetch = globalThis.fetch;
const shutdown = new AbortController();
const fetchSpy = vi.fn(
(_input: RequestInfo | URL, init?: RequestInit) =>
new Promise<AbortSignal>((resolve) => {
const signal = init?.signal as AbortSignal;
signal.addEventListener("abort", () => resolve(signal), { once: true });
}),
);
globalThis.fetch = fetchSpy as unknown as typeof fetch;
try {
botCtorSpy.mockClear();
createTelegramBot({ token: "tok", fetchAbortSignal: shutdown.signal });
const clientFetch = (botCtorSpy.mock.calls.at(-1)?.[1] as { client?: { fetch?: unknown } })
?.client?.fetch as (input: RequestInfo | URL, init?: RequestInit) => Promise<unknown>;
expect(clientFetch).toBeTypeOf("function");
const observedSignalPromise = clientFetch("https://example.test");
shutdown.abort(new Error("shutdown"));
const observedSignal = (await observedSignalPromise) as AbortSignal;
expect(observedSignal).toBeInstanceOf(AbortSignal);
expect(observedSignal.aborted).toBe(true);
} finally {
globalThis.fetch = originalFetch;
}
});
});

View File

@@ -110,8 +110,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
// (especially long-polling getUpdates) aborts immediately on shutdown. Without this,
// the in-flight getUpdates hangs for up to 30s, and a new gateway instance starting
// its own poll triggers a 409 Conflict from Telegram.
let finalFetch: NonNullable<ApiClientOptions["fetch"]> | undefined =
shouldProvideFetch && fetchImpl ? fetchForClient : undefined;
let finalFetch = shouldProvideFetch && fetchImpl ? fetchForClient : undefined;
if (opts.fetchAbortSignal) {
const baseFetch =
finalFetch ?? (globalThis.fetch as unknown as NonNullable<ApiClientOptions["fetch"]>);