Fix server crashes from unhandled push-notification errors

Root cause of the regressions reported after the Web Push feature
landed (chat messages not appearing until refresh, notifications
stopping entirely — even in-page toast/sound, which has nothing to do
with push): the fire-and-forget push call in appendMessage() had no
.catch(). This app has no global unhandledRejection handler, and Node
terminates the whole process on an unhandled rejection by default — so
any error inside notifyPushForCustomerMessage() (e.g. the unguarded
db.query.users.findMany() call, which can hit SQLITE_BUSY under
concurrent load, something this exact codebase has already hit
elsewhere) would crash the entire server on every customer message,
not just fail that one push send. Docker's restart policy masked it as
"weird flakiness" rather than an obvious crash loop.

Two fixes: the push call now has a .catch() so a failure there can
never escape as an unhandled rejection, and — as a general safety net,
since this codebase had no equivalent to project-claude's
uncaughtException/unhandledRejection handlers — instrumentation.ts now
logs-and-continues on both, so a future bug in any other fire-and-forget
path can't take the whole server down either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-08-07 23:26:06 +00:00
1 parent b9a247c21c
commit 71c0689af9
2 files changed
+22 -2

No files matched your search

+14
View File
@@ -1,5 +1,19 @@
export async function register() { export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") { if (process.env.NEXT_RUNTIME === "nodejs") {
// Node terminates the whole process on an unhandled promise rejection
// by default — one bug in any best-effort/fire-and-forget path (push
// notifications, a background job, ...) would otherwise take down every
// in-flight request, not just the one that triggered it. Log-and-continue
// is the right call here: anything that reaches this handler was already
// meant to be non-fatal (a real, must-not-fail error should be caught and
// handled at its own call site instead).
process.on("uncaughtException", (err) => {
console.error("[process] uncaughtException (ignored, server continues)", err);
});
process.on("unhandledRejection", (reason) => {
console.error("[process] unhandledRejection (ignored, server continues)", reason);
});
const { ensureTelegramBotStarted } = await import("@/lib/telegram/bot"); const { ensureTelegramBotStarted } = await import("@/lib/telegram/bot");
await ensureTelegramBotStarted(); await ensureTelegramBotStarted();
+8 -2
View File
@@ -169,9 +169,15 @@ async function appendMessage(params: {
// Fire-and-forget: push delivery is a best-effort side channel for // Fire-and-forget: push delivery is a best-effort side channel for
// backgrounded/frozen tabs, not part of the message-creation contract — // backgrounded/frozen tabs, not part of the message-creation contract —
// callers (mail ingestion, the API routes) must not wait on or fail from // callers (mail ingestion, the API routes) must not wait on or fail from
// a slow/unreachable push service. // a slow/unreachable push service. The .catch() here is load-bearing:
// this app has no global unhandledRejection handler, so an uncaught
// rejection from this call would otherwise crash the whole Node process
// (Node terminates on unhandled rejections by default) — taking down
// every in-flight request, not just this one message.
if (params.authorType === "customer") { if (params.authorType === "customer") {
void notifyPushForCustomerMessage(dto, messageDto, params.ticketIsNew); notifyPushForCustomerMessage(dto, messageDto, params.ticketIsNew).catch((err) => {
console.error("[push] notifyPushForCustomerMessage failed:", err);
});
} }
return { ticket: dto, message: messageDto }; return { ticket: dto, message: messageDto };