From 39c9ad9615310df1521c6a6109b08f03d0a71f73 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 1 Sep 2026 04:27:08 -0700 Subject: [PATCH] fix(webhooks): await the Telegram provider handler so the dedup claim is released MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1422 merged at e794b1fc with this outstanding. The last exit of the POST handler's try block was the only un-awaited return of 14: return events(req, res); `events` is async and awaits a Mongo write (telegramProvider.ts:111, :126), and `return promise` inside a try does not reach that try's catch — only `return await promise` does. express is 4.21.2 here with no express-async-errors and no asyncHandler wrapper, so a rejection from the provider escapes the route entirely: the forget-on-error release never runs, no response is sent, and Telegram's redelivery is then acked 200 as a duplicate and dropped permanently. That is the exact swallowed-drop the release contract exists to prevent — the comment on the catch says so directly ("the claim must not survive to swallow that retry") — and it is new behaviour, because before the dedup claim existed the redelivery simply reprocessed. The liveRelay branch immediately above already awaits its bridge call, so this was the one escaping path. The 15 shipped tests cannot see it: every test reaching this line stubs `events` synchronously, while the real one is async. The added test stubs it async and rejecting. Without the fix it times out — no response is ever sent, which is the defect's own signature; with the fix, 16/16 pass. Co-Authored-By: Claude Opus 5 --- .../unit/routes/telegram.webhook.test.js | 41 +++++++++++++++++++ backend/routes/webhooks/telegram.ts | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/backend/__tests__/unit/routes/telegram.webhook.test.js b/backend/__tests__/unit/routes/telegram.webhook.test.js index 9d38eb5500..dc9703b8bc 100644 --- a/backend/__tests__/unit/routes/telegram.webhook.test.js +++ b/backend/__tests__/unit/routes/telegram.webhook.test.js @@ -165,6 +165,47 @@ describe('Telegram webhook routes', () => { expect(res.status).toBe(200); expect(events).toHaveBeenCalled(); }); + + it('releases the dedup claim when the async provider handler rejects', async () => { + // The provider's real `events` is async (telegramProvider.ts:111 awaits a + // Mongo write), so it can reject AFTER the route has claimed the update_id. + // With a bare `return events(...)` the rejection escapes the try/catch — + // express 4 has no async error handling — so the claim is never released, + // no response is sent, and Telegram's redelivery is then acked 200 as a + // duplicate and dropped permanently. Stubbing `events` synchronously, as + // the tests above do, cannot see this. + const integration = { + _id: 'integration-1', + type: 'telegram', + config: { chatId: '42' }, + }; + Integration.findOne.mockResolvedValue(integration); + const events = jest.fn(async () => { + throw new Error('provider write failed'); + }); + registry.get.mockReturnValue({ + getWebhookHandlers: () => ({ events }), + }); + + const res = await request(app) + .post('/api/webhooks/telegram') + .send({ + update_id: 5150, + message: { + text: 'hello', + chat: { id: 42, title: 'Test Chat', type: 'group' }, + from: { id: 7, first_name: 'Sam' }, + }, + }); + + const WebhookDelivery = require('../../../models/WebhookDelivery'); + expect(events).toHaveBeenCalled(); + expect(res.status).toBe(500); + expect(WebhookDelivery.deleteOne).toHaveBeenCalledWith({ + provider: 'telegram', + deliveryId: '5150', + }); + }); describe('live relay ack', () => { const liveIntegration = { _id: 'integration-1', diff --git a/backend/routes/webhooks/telegram.ts b/backend/routes/webhooks/telegram.ts index 6cc220a4da..dd2cfcfdd2 100644 --- a/backend/routes/webhooks/telegram.ts +++ b/backend/routes/webhooks/telegram.ts @@ -462,7 +462,7 @@ router.post('/', async (req: any, res: any) => { const provider = registry.get('telegram', integration); const { events } = provider.getWebhookHandlers(); - return events(req, res); + return await events(req, res); } catch (error) { console.error('Telegram webhook error', error); // Forget-on-error: the 500 makes Telegram redeliver this update_id; the