From ee3c38972b4bdb871eed2ec14acfeeb357b41088 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 6 Jul 2026 00:14:56 -0400 Subject: [PATCH] fix: reject a non-str stored token as stale before rebuilding the webhook (dpywebhooks-3) Webhook.partial does no token validation of its own, so a store record with token=None (e.g. a mongo/file store deserializing a missing field as null) rebuilt and cached a token-less webhook with no error. Webhook.send then raised a raw ValueError, not HTTPException, so send()'s dead-webhook except never fired and the poisoned record was never cleared - the channel got permanently stuck. Validate the token type before partial() so a bad record takes the same clear-and-fall-through path as any other stale record. Signed-off-by: disqualifier --- src/dpy_webhooks/dpy_webhooks.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/dpy_webhooks/dpy_webhooks.py b/src/dpy_webhooks/dpy_webhooks.py index 0ab6d4d..aedbebb 100644 --- a/src/dpy_webhooks/dpy_webhooks.py +++ b/src/dpy_webhooks/dpy_webhooks.py @@ -351,11 +351,18 @@ class DPYWebhooks: async def _restore_from_store(self, channel: discord.TextChannel) -> "Optional[discord.Webhook]": """rebuild a partial webhook from the stored record; on a rebuild fault, treat the - record as stale (clear + return None so the caller can recreate)""" + record as stale (clear + return None so the caller can recreate) + + Webhook.partial does no token validation of its own and will happily cache a + token-less webhook, which then raises a raw ValueError (not HTTPException) on + send - bypassing the self-heal path entirely and getting stuck forever. reject a + non-str token here, before partial(), so that record is treated as stale too""" record = await self._store.get(channel.id) if not record: return None try: + if not isinstance(record["token"], str): + raise TypeError(f"stored token must be str, got {type(record['token']).__name__}") webhook = discord.Webhook.partial( int(record["webhook_id"]), record["token"],