From ef799056ecfcd2270a3cf73b03648e9a52a3cb96 Mon Sep 17 00:00:00 2001 From: Dhereal1 Date: Thu, 2 Apr 2026 22:17:42 +0100 Subject: [PATCH 1/2] fix(bot): reset init promise on failure to prevent dead bot instance --- app/bot/webhook.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/bot/webhook.ts b/app/bot/webhook.ts index 55eb581..ded0647 100644 --- a/app/bot/webhook.ts +++ b/app/bot/webhook.ts @@ -46,7 +46,19 @@ const bot = BOT_TOKEN ? createBot(BOT_TOKEN) : null; let botInitPromise: Promise | null = null; function ensureBotInit(): Promise { if (!bot) return Promise.resolve(); - if (!botInitPromise) botInitPromise = bot.init(); + if (!botInitPromise) { + try { + botInitPromise = bot.init().catch((err) => { + console.error('[bot] init failed', err); + botInitPromise = null; + throw err; + }); + } catch (err) { + console.error('[bot] init failed', err); + botInitPromise = null; + throw err; + } + } return botInitPromise; } From 9dc3318731facee703cb91996674187ed5197ca9 Mon Sep 17 00:00:00 2001 From: Dhereal1 Date: Thu, 2 Apr 2026 23:00:11 +0100 Subject: [PATCH 2/2] test(bot): ensure init retries after failure --- app/bot/webhook.ensureBotInit.test.ts | 83 +++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 app/bot/webhook.ensureBotInit.test.ts diff --git a/app/bot/webhook.ensureBotInit.test.ts b/app/bot/webhook.ensureBotInit.test.ts new file mode 100644 index 0000000..848b400 --- /dev/null +++ b/app/bot/webhook.ensureBotInit.test.ts @@ -0,0 +1,83 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { Bot } from 'grammy'; + +test('ensureBotInit resets after init failure (no stuck rejected promise)', async (t) => { + const originalInit = Bot.prototype.init; + const originalHandleUpdate = Bot.prototype.handleUpdate; + + let initCalls = 0; + let handleUpdateCalls = 0; + + Bot.prototype.init = async function initMock(): Promise { + initCalls += 1; + if (initCalls === 1) throw new Error('init failed (simulated)'); + }; + + Bot.prototype.handleUpdate = async function handleUpdateMock(): Promise { + handleUpdateCalls += 1; + }; + + t.after(() => { + Bot.prototype.init = originalInit; + Bot.prototype.handleUpdate = originalHandleUpdate; + delete process.env.BOT_TOKEN; + delete process.env.TELEGRAM_BOT_TOKEN; + }); + + process.env.BOT_TOKEN = 'test-token'; + + // Import after env + prototype mocks so the module creates a bot using the patched methods. + const mod = await import('./webhook.ts'); + const handler = mod.default as ( + req: { method: string; body?: unknown }, + res: { + setHeader(name: string, value: string): void; + status(code: number): { json(data: unknown): void; end(): void }; + end(): void; + }, + ) => Promise; + + function createRes() { + let statusCode: number | null = null; + let jsonBody: unknown = undefined; + return { + get statusCode() { + return statusCode; + }, + get jsonBody() { + return jsonBody; + }, + res: { + setHeader() {}, + status(code: number) { + statusCode = code; + return { + json(data: unknown) { + jsonBody = data; + }, + end() {}, + }; + }, + end() {}, + }, + }; + } + + const update = JSON.stringify({ + update_id: 1, + message: { chat: { id: 123 }, text: 'hi' }, + }); + + const first = createRes(); + await handler({ method: 'POST', body: update }, first.res); + assert.equal(first.statusCode, 500); + + const second = createRes(); + await handler({ method: 'POST', body: update }, second.res); + assert.equal(second.statusCode, 200); + + assert.equal(initCalls, 2); + assert.equal(handleUpdateCalls, 1); +}); +