Skip to content

fix(bot): stop retrying a webhook delivery forever when it throws - #313

Merged
Musiker15 merged 1 commit into
mainfrom
fix/bot-webhook-poison-row
Nov 5, 2026
Merged

Musiker15 merged 1 commit into
mainfrom
fix/bot-webhook-poison-row

Conversation

@Musiker15

Copy link
Copy Markdown
Member

Description

One webhook delivery in production had been retried every 15 seconds since Nov 2 (about 3,900 log lines today alone) with Cannot read properties of undefined (reading 'length').

Cause: the submission behind a Discord submission.created delivery was deleted before the delivery went out. hydratePayload then fell back to the thin outbox payload, and buildDiscordWebhookBody called truncate() on its undefined status. The exception was caught by the poll loop, but the row never had its attempts counted or nextAttemptAt moved, so it stayed due on every tick.

Changes:

  • apps/bot/src/webhooks.ts: Discord deliveries whose submission no longer exists are marked failed with "Submission no longer exists.". Generic JSON hooks still get the stored thin payload, as before. An exception during a delivery now goes through recordCrash, which counts it as a failed attempt with backoff.
  • apps/bot/src/webhook-retry.ts: failedAttempt() (attempt count, backoff, give-up at MAX_ATTEMPTS) and backoffMs, moved out of webhooks.ts so they can be tested without a database. The normal failure path uses the same helper.
  • apps/bot/src/webhook-retry.test.ts: tests, including a row that fails on every tick ending up failed after MAX_ATTEMPTS.

The stuck production row will be marked failed on the first tick after deploy, no manual database change needed.

Type of change

  • Bugfix
  • New feature
  • Breaking change
  • Refactor / tech debt
  • Docs / CI

Checklist

  • pnpm lint, pnpm typecheck, pnpm build pass locally (bot package: lint, typecheck, tests; the rest is left to CI)
  • Tests added/updated (where it makes sense). Counter-check: without incrementing attempts, three of the four tests fail
  • Prisma migration included (if the schema changed), not needed
  • No secrets / .env values committed
  • Security implications considered (input validation, authz, CSP)
  • Docs updated if architecture is affected, not affected

Linked issues

None

A Discord submission webhook whose submission had been deleted fell back to
the thin outbox payload. buildDiscordWebhookBody then called truncate() on an
undefined status and threw. The poll loop caught the error, but the row never
had its attempts counted or its next attempt moved, so it was retried on every
tick. One ATLAS delivery had been retried every 15 seconds since Nov 2.

- Discord deliveries whose submission no longer exists are marked failed with
  "Submission no longer exists." Generic JSON hooks keep getting the stored
  thin payload, as before.
- Any exception during a delivery now counts as a failed attempt with backoff,
  so a poisoned row ends up failed after MAX_ATTEMPTS.
- Retry state lives in a pure helper (webhook-retry.ts) with tests.
@graphify-labs

graphify-labs Bot commented Nov 5, 2026

Copy link
Copy Markdown

Graphify reviews are paused for this workspace because nobody has used it in 7 days. A workspace owner can resume reviews from the Repositories page: https://app.graphify.com/msk-scripts/repositories

@Musiker15
Musiker15 merged commit 0af36a4 into main Nov 5, 2026
4 checks passed
@Musiker15
Musiker15 deleted the fix/bot-webhook-poison-row branch November 5, 2026 15:27
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