mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 16:27:26 +00:00
fix: abort telegram getupdates on shutdown (#23950) (thanks @Gkinthecodeland)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
34
src/telegram/bot.fetch-abort.test.ts
Normal file
34
src/telegram/bot.fetch-abort.test.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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"]>);
|
||||
|
||||
Reference in New Issue
Block a user