From 1c3003421e19d7ee5e4ecea93be02a8351e67063 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 1 May 2026 00:54:40 +0200 Subject: [PATCH] fix: extend timeout for PushVerification and clean up leftover subscriptions --- lib/web-push.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/web-push.ts b/lib/web-push.ts index fa03253c..bfa8a053 100644 --- a/lib/web-push.ts +++ b/lib/web-push.ts @@ -168,7 +168,11 @@ async function pollVerificationCode( relayBaseUrl: string, subscriptionId: string, ): Promise { - const timeoutAt = Date.now() + 20_000; + // Stalwart per-account rate-limits PushVerification posts (default 60s). + // If there are leftover unverified subscriptions on the account, our new + // one queues up behind them - so we wait long enough to clear one verify + // window even in the unlucky case. + const timeoutAt = Date.now() + 75_000; let delay = 400; while (Date.now() < timeoutAt) { const res = await fetch( @@ -254,10 +258,10 @@ export async function enableWebPush( // Reuse the JMAP-side PushSubscription if the server still has it, just // refreshing the expiry so it doesn't time out between sessions. + const existingSubs = await params.client.listPushSubscriptions().catch(() => []); const storedServerId = localStorage.getItem(SUBSCRIPTION_ID_KEY); if (storedServerId) { - const existing = await params.client.listPushSubscriptions().catch(() => []); - const match = existing.find((s) => s.id === storedServerId); + const match = existingSubs.find((s) => s.id === storedServerId); if (match) { const refreshed = await refreshSubscriptionExpires(params.client, match); if (refreshed) return { subscriptionId: storedServerId }; @@ -266,6 +270,17 @@ export async function enableWebPush( localStorage.removeItem(SUBSCRIPTION_ID_KEY); } + // Reap any leftover subscriptions still bound to this device. These pile + // up when a previous enable attempt failed mid-flow (verification timed + // out, browser tab closed, etc). Stalwart per-account rate-limits + // verification posts, so leaving stragglers around blocks the new one. + const stragglers = existingSubs.filter( + (s) => s.deviceClientId === deviceClientId && s.id !== storedServerId, + ); + for (const s of stragglers) { + await params.client.destroyPushSubscription(s.id).catch(() => undefined); + } + const serverAssignedId = await params.client.createPushSubscription({ deviceClientId, url: buildRelayUrl(relayBaseUrl, `/api/push/jmap/${encodeURIComponent(deviceClientId)}`),