[object Object]

← back to Archive Agent

fix: wrap all async DB route handlers in try/catch to prevent process crash on PG error

8c0551149007ec52a2df6de01fc00075f68f8686 · 2026-05-30 21:23:02 -0700 · SteveStudio2

GET /, GET /alerts, GET /summary, and POST /ack were missing error handling — an
unhandled rejected promise from pg-pool caused Node to crash the process instead of
returning a 500. Added try/catch to each handler so DB failures return HTTP 500
without killing the server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 8c0551149007ec52a2df6de01fc00075f68f8686
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Sat May 30 21:23:02 2026 -0700

    fix: wrap all async DB route handlers in try/catch to prevent process crash on PG error
    
    GET /, GET /alerts, GET /summary, and POST /ack were missing error handling — an
    unhandled rejected promise from pg-pool caused Node to crash the process instead of
    returning a 500. Added try/catch to each handler so DB failures return HTTP 500
    without killing the server.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 server.js | 86 +++++++++++++++++++++++++++++++++++++--------------------------
 1 file changed, 51 insertions(+), 35 deletions(-)

diff --git a/server.js b/server.js
index 1ad9d6a..baded66 100644
--- a/server.js
+++ b/server.js
@@ -245,39 +245,47 @@ app.post('/scan', async (req, res) => {
 });
 
 app.get('/alerts', async (req, res) => {
-  const cls = req.query.class || null;
-  const vendor = req.query.vendor || null;
-  const acknowledged = req.query.ack === '1';
-  const limit = Math.min(parseInt(req.query.limit || '500', 10), 5000);
-  const params = [];
-  const where = [];
-  if (cls) { params.push(cls); where.push(`classification = $${params.length}`); }
-  if (vendor) { params.push(vendor); where.push(`vendor = $${params.length}`); }
-  where.push(`acknowledged_at IS ${acknowledged ? 'NOT NULL' : 'NULL'}`);
-  params.push(limit);
-  const { rows } = await pool.query(
-    `SELECT * FROM archive_alerts WHERE ${where.join(' AND ')} ORDER BY detected_at DESC LIMIT $${params.length}`,
-    params
-  );
-  res.json({ count: rows.length, rows });
+  try {
+    const cls = req.query.class || null;
+    const vendor = req.query.vendor || null;
+    const acknowledged = req.query.ack === '1';
+    const limit = Math.min(parseInt(req.query.limit || '500', 10), 5000);
+    const params = [];
+    const where = [];
+    if (cls) { params.push(cls); where.push(`classification = $${params.length}`); }
+    if (vendor) { params.push(vendor); where.push(`vendor = $${params.length}`); }
+    where.push(`acknowledged_at IS ${acknowledged ? 'NOT NULL' : 'NULL'}`);
+    params.push(limit);
+    const { rows } = await pool.query(
+      `SELECT * FROM archive_alerts WHERE ${where.join(' AND ')} ORDER BY detected_at DESC LIMIT $${params.length}`,
+      params
+    );
+    res.json({ count: rows.length, rows });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
 });
 
 app.get('/summary', async (req, res) => {
-  const { rows: byClass } = await pool.query(`
-    SELECT classification, COUNT(*)::int AS n,
-      COUNT(*) FILTER (WHERE acknowledged_at IS NULL)::int AS unacked
-    FROM archive_alerts GROUP BY 1 ORDER BY n DESC`);
-  const { rows: byVendor } = await pool.query(`
-    SELECT vendor, COUNT(*)::int AS n,
-      COUNT(*) FILTER (WHERE classification IN ('WRONG_PRICED','WRONG_SAMPLE','DRIFT'))::int AS wrongful
-    FROM archive_alerts WHERE acknowledged_at IS NULL
-    GROUP BY 1 ORDER BY wrongful DESC, n DESC LIMIT 30`);
-  const { rows: byDay } = await pool.query(`
-    SELECT DATE(shopify_updated) AS day, COUNT(*)::int AS n,
-      COUNT(*) FILTER (WHERE classification IN ('WRONG_PRICED','WRONG_SAMPLE','DRIFT'))::int AS wrongful
-    FROM archive_alerts WHERE shopify_updated > now() - interval '30 days'
-    GROUP BY 1 ORDER BY 1 DESC`);
-  res.json({ lastScan, scanResult, byClass, byVendor, byDay });
+  try {
+    const { rows: byClass } = await pool.query(`
+      SELECT classification, COUNT(*)::int AS n,
+        COUNT(*) FILTER (WHERE acknowledged_at IS NULL)::int AS unacked
+      FROM archive_alerts GROUP BY 1 ORDER BY n DESC`);
+    const { rows: byVendor } = await pool.query(`
+      SELECT vendor, COUNT(*)::int AS n,
+        COUNT(*) FILTER (WHERE classification IN ('WRONG_PRICED','WRONG_SAMPLE','DRIFT'))::int AS wrongful
+      FROM archive_alerts WHERE acknowledged_at IS NULL
+      GROUP BY 1 ORDER BY wrongful DESC, n DESC LIMIT 30`);
+    const { rows: byDay } = await pool.query(`
+      SELECT DATE(shopify_updated) AS day, COUNT(*)::int AS n,
+        COUNT(*) FILTER (WHERE classification IN ('WRONG_PRICED','WRONG_SAMPLE','DRIFT'))::int AS wrongful
+      FROM archive_alerts WHERE shopify_updated > now() - interval '30 days'
+      GROUP BY 1 ORDER BY 1 DESC`);
+    res.json({ lastScan, scanResult, byClass, byVendor, byDay });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
 });
 
 app.post('/restore', async (req, res) => {
@@ -289,16 +297,21 @@ app.post('/restore', async (req, res) => {
 });
 
 app.post('/ack', async (req, res) => {
-  const ids = req.body?.ids;
-  const by = req.body?.by || 'manual';
-  if (!Array.isArray(ids) || !ids.length) return res.status(400).json({ error: 'ids[] required' });
-  await pool.query(`UPDATE archive_alerts SET acknowledged_at = now(), acknowledged_by=$1 WHERE shopify_id = ANY($2::text[])`, [by, ids]);
-  res.json({ ok: true, acked: ids.length });
+  try {
+    const ids = req.body?.ids;
+    const by = req.body?.by || 'manual';
+    if (!Array.isArray(ids) || !ids.length) return res.status(400).json({ error: 'ids[] required' });
+    await pool.query(`UPDATE archive_alerts SET acknowledged_at = now(), acknowledged_by=$1 WHERE shopify_id = ANY($2::text[])`, [by, ids]);
+    res.json({ ok: true, acked: ids.length });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
 });
 
 app.get('/owner/:vendor', (req, res) => res.json({ vendor: req.params.vendor, owner: ownerFor(req.params.vendor) }));
 
 app.get('/', async (req, res) => {
+  try {
   const { rows: byClass } = await pool.query(`
     SELECT classification, COUNT(*)::int AS n,
       COUNT(*) FILTER (WHERE acknowledged_at IS NULL)::int AS unacked
@@ -364,6 +377,9 @@ ${recent.map(r => `<tr>
 
 <p class="meta">API: <code>GET /summary</code> · <code>GET /alerts?class=WRONG_PRICED</code> · <code>POST /scan {days:N}</code> · <code>POST /restore {ids:[...]}</code> · <code>POST /ack {ids:[...]}</code></p>
 </body></html>`);
+  } catch (e) {
+    res.status(500).send(`<pre>DB error: ${e.message}</pre>`);
+  }
 });
 
 // ---------- Bootstrap ----------

← 6f849da security: strip hardcoded dw_admin DSN password -> env-first  ·  back to Archive Agent  ·  auto-save: 2026-06-21T18:53:00 (2 files) — ecosystem.config. 6ab943e →