Skip to content

fix(webhooks): await the Telegram provider handler so the dedup claim is released - #1429

Closed
lilyshen0722 wants to merge 1 commit into
mainfrom
fix/telegram-await-events
Closed

lilyshen0722 wants to merge 1 commit into
mainfrom
fix/telegram-await-events

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1422, which merged at e794b1fc with this outstanding. Verified present on origin/main at backend/routes/webhooks/telegram.ts:465.

The defect

The last exit of the POST handler's try block is the only un-awaited return of the 14 in it:

return events(req, res);

Four links, each measured rather than reasoned:

  1. events is async and can rejecttelegramProvider.ts:111 declares it async, :126 awaits Integration.findByIdAndUpdate.
  2. return promise inside a try does not reach that catch — only return await promise does. Verified in isolation on node 22.
  3. Nothing else catches it — express is 4.21.2, no express-async-errors, no asyncHandler wrapper anywhere in backend/.
  4. So the rejection escapes the route. The forget-on-error release never runs, no response is sent, Telegram redelivers, claimDelivery returns 'duplicate', and the update is acked 200 and dropped permanently.

That is precisely what the release contract exists to prevent — the comment on the catch says it outright: "the claim must not survive to swallow that retry." And it is new behaviour: before the dedup claim existed, the redelivery just reprocessed. The liveRelay branch immediately above already awaits its bridge call, so this was the single escaping path.

Why 15/15 was green

Every test that reaches this line stubs the handler synchronously:

const events = jest.fn((req, res) => res.sendStatus(200));

while the real one is async. The mock differs from production in exactly the dimension the bug lives in, so the suite could not have caught it at any coverage level.

The added test

Stubs events async and rejecting, then asserts the release fired and a 500 went out. The differential:

Result
without await fails — times out, because no response is ever sent (the defect's own signature)
with await 16/16 pass

Run on node 22. I reverted the one-word fix and re-ran to confirm the test actually discriminates, rather than trusting that a new test which passes is testing anything.

Not fixed here

Two things I raised on #1422 that are deliberately out of scope for a one-word fix, both worth their own change:

  • Both updateId !== undefined && updateId !== null guards are unpinned — deleting either one survives the suite. Without them an update with no update_id claims the literal key 'undefined', and every subsequent one is acked and dropped. Ten of the fifteen shipped tests post exactly that shape and all stay green.
  • The deploy prerequisite is still unmet. TELEGRAM_SECRET_TOKEN and TELEGRAM_WEBHOOK_ALLOW_UNVERIFIED appear nowhere under k8s/, while TELEGRAM_BOT_TOKEN is wired (backend-deployment.yaml:356). The bridge is provisioned, so deploying as-is fail-closes every inbound update. Chart change and setWebhook(secret_token=…) need to land before this reaches the cluster.

🤖 Generated with Claude Code

… is released

#1422 merged at e794b1f 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 <noreply@anthropic.com>
@samxu01
samxu01 force-pushed the fix/telegram-await-events branch from 5ad592e to 39c9ad9 Compare September 1, 2026 17:44
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Rebased onto a299b6d3 and force-pushed to 39c9ad96. The only failure here was the inherited agentUserShapeContract red, which #1431 fixed on main; this branch was carrying the pre-#1431 copy of that test, so a check re-run alone would not have cleared it — the base had to move.

Re-verified rather than assumed, six hours on:

  • The defect is still present on current main — backend/routes/webhooks/telegram.ts:465, return events(req, res), un-awaited.
  • The diff is unchanged by the rebase: still +42/-1 across two files.
  • 25/25 green locally on node 22 (the telegram suite plus the guard that was red).

Merge-order note: #1297 (fix(telegram): close the connector create/enable holes ahead of ADR-025, P0) also touches this file, and git merge-tree reports the pair as conflicting. It was last updated 2026-08-30 and needs a rebase against main regardless. Mine is one word plus a test, so it is much cheaper to re-land — press this first and #1297's rebase absorbs it, rather than the other way round.

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Superseded by #1449, which carries @sprint-impl's 50629076 — the same one-word fix plus a test pinning the no-update_id path, which I had explicitly left out of scope here. Theirs pins strictly more, so closing mine rather than making the better PR go dirty.

Their commit was stranded: it sat on #1422's branch, and #1422 is merged and squash-merged, so a PR from there would have replayed all 276 lines and conflicted. #1449 is that commit cherry-picked onto current main — 60 insertions, applies clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant