From c2d5873f087f962c8e7c4f2825ff21372cb68e2d Mon Sep 17 00:00:00 2001 From: null Date: Wed, 13 May 2026 19:59:19 -0500 Subject: [PATCH] feat: error handling hardening, 404 catch-all, health check DB test, request timeout, global error handlers (v0.4.8) --- README.md | 2 +- package.json | 2 +- server/index.js | 64 +++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6781679..dd30173 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ Version numbers correlate directly to the active phase: - ~~Server-side validation + input sanitization~~ ✅ - ~~Optional Zoho forwarding layer~~ ✅ - ~~Rate limiting + security headers + CORS~~ ✅ - - Backend/API hardening as needed + - ~~Backend/API hardening as needed~~ ✅ - **Phase 5 — Verification + Release Readiness**: `0.5.x` - Build/runtime verification diff --git a/package.json b/package.json index c228806..123c213 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "queuenorth-website", "private": true, - "version": "0.4.7", + "version": "0.4.8", "type": "module", "scripts": { "dev": "concurrently \"vite\" \"node server/index.js\"", diff --git a/server/index.js b/server/index.js index 414098c..70348e8 100644 --- a/server/index.js +++ b/server/index.js @@ -121,8 +121,6 @@ app.use((req, res, next) => { next() }) -app.use(express.static(path.join(__dirname, '../dist'))) - // --- Database --- const db = sqlite3(dbPath) @@ -300,14 +298,21 @@ async function forwardToZoho(leadData) { // Health check app.get('/api/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }) + try { + // Verify DB connection by executing a simple query + db.prepare('SELECT 1').get() + res.json({ status: 'ok', db: 'ok', timestamp: new Date().toISOString() }) + } catch (err) { + log.error('Health check DB verification failed:', err.message) + res.status(503).json({ error: 'Service unavailable', db: 'error', timestamp: new Date().toISOString() }) + } }) // Submit lead app.post('/api/leads', (req, res) => { try { const parsed = leadSchema.safeParse(req.body) - + if (!parsed.success) { const fieldErrors = {} for (const issue of parsed.error.issues) { @@ -352,7 +357,7 @@ app.post('/api/leads', (req, res) => { // Fire-and-forget Zoho forwarding (best-effort, non-blocking) forwardToZoho(sanitized) - res.json({ success: true, message: 'Thanks! We\'ll be in touch shortly.' }) + res.json({ success: true, message: "Thanks! We'll be in touch shortly." }) } catch (err) { log.error('Error submitting lead:', err) res.status(500).json({ error: 'Failed to submit lead' }) @@ -363,7 +368,7 @@ app.post('/api/leads', (req, res) => { app.post('/api/support', (req, res) => { try { const parsed = supportSchema.safeParse(req.body) - + if (!parsed.success) { const fieldErrors = {} for (const issue of parsed.error.issues) { @@ -403,13 +408,58 @@ app.post('/api/support', (req, res) => { log.info(`Support request submitted: ${sanitized.email} from ${sanitized.company} priority=${sanitized.priority || 'medium'} (id: ${result.lastInsertRowid})`) - res.json({ success: true, message: 'Thanks! We\'ll get back to you soon.' }) + res.json({ success: true, message: "Thanks! We'll get back to you soon." }) } catch (err) { log.error('Error submitting support request:', err) res.status(500).json({ error: 'Failed to submit support request' }) } }) +// --- 404 catch-all for API routes (must be after all API routes) --- +app.use((req, res, next) => { + if (req.path.startsWith('/api')) { + log.warn(`API route not found: ${req.method} ${req.originalUrl}`) + return res.status(404).json({ error: 'Not found' }) + } + next() +}) + +// Static file serving for SPA (falls through to SPA router) +app.use(express.static(path.join(__dirname, '../dist'))) + +// --- Request timeout middleware (30 seconds) --- +const REQUEST_TIMEOUT_MS = 30000 + +const timeoutMiddleware = (req, res, next) => { + const timeout = setTimeout(() => { + if (!res.headersSent) { + log.warn(`Request timeout: ${req.method} ${req.originalUrl}`) + res.status(504).json({ error: 'Request timeout' }) + } + }, REQUEST_TIMEOUT_MS) + + res.on('finish', () => clearTimeout(timeout)) + res.on('close', () => clearTimeout(timeout)) + next() +} + +app.use(timeoutMiddleware) + +// --- Global error handlers --- +process.on('uncaughtException', (err) => { + log.error('Uncaught exception:', err.message) + log.error('Stack:', err.stack) + log.error('Shutting down due to uncaught exception...') + process.exit(1) +}) + +process.on('unhandledRejection', (reason, promise) => { + log.error('Unhandled rejection at:', promise) + log.error('Reason:', reason) + log.error('Shutting down due to unhandled rejection...') + process.exit(1) +}) + // --- Start Server --- const PORT = process.env.SERVER_PORT || 3001