From 3f97e6ed8d2a6d4ed10c611a7db4f203c09b39ef Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 1 May 2026 02:08:13 +0200 Subject: [PATCH] fix: scope email notifications to genuine inbox deliveries --- app/api/push/preview/route.ts | 7 ++++++- public/sw.js | 15 +++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/api/push/preview/route.ts b/app/api/push/preview/route.ts index 0bbae2f7..32c6bb08 100644 --- a/app/api/push/preview/route.ts +++ b/app/api/push/preview/route.ts @@ -127,8 +127,13 @@ export async function GET(request: NextRequest) { }, }); } catch (error) { + // `fetch failed` from undici is too generic to debug — the real reason + // (ENOTFOUND, ECONNREFUSED, TLS error, …) is on `error.cause`. + const err = error as Error & { cause?: { code?: string; message?: string } }; logger.error('push preview failed', { - error: error instanceof Error ? error.message : 'Unknown error', + error: err?.message ?? 'Unknown error', + causeCode: err?.cause?.code, + causeMessage: err?.cause?.message, }); return NextResponse.json({ error: 'Internal error' }, { status: 500 }); } diff --git a/public/sw.js b/public/sw.js index f7cd72f4..9aea35e6 100644 --- a/public/sw.js +++ b/public/sw.js @@ -46,6 +46,7 @@ async function handlePush(event) { // expired, server down) we fall back to a generic "New mail" so the user // still sees something. let preview = null; + let previewOk = false; try { const res = await fetch("/api/push/preview", { credentials: "include", @@ -53,6 +54,7 @@ async function handlePush(event) { }); if (res.ok) { preview = await res.json(); + previewOk = true; } } catch (_) { preview = null; @@ -63,12 +65,13 @@ async function handlePush(event) { ? preview.unreadTotal : 0; - // The push subscription is scoped to EmailDelivery, but we still see the - // occasional wake-up that does not correspond to a new unread message - // (legacy subscription with broader types, races with marking-as-read, - // verification pings). Without a concrete unread email to surface, stay - // silent rather than firing a generic "New mail" toast for a non-event. - if (!email && unreadTotal === 0) { + // Push subscription is scoped to EmailDelivery, but stragglers from the + // older broader-types subscription, marking-as-read races and verification + // pings can still wake us with no actual unread mail. When the preview API + // succeeded and reports zero unread, stay silent. When the preview API + // failed (network/auth/server down) we cannot tell, so fall through to the + // generic "New mail" toast rather than miss a real delivery. + if (previewOk && !email && unreadTotal === 0) { return; }