feat: error handling hardening, 404 catch-all, health check DB test, request timeout, global error handlers (v0.4.8)

This commit is contained in:
null 2026-05-13 19:59:19 -05:00
parent 7257633d94
commit c2d5873f08
3 changed files with 59 additions and 9 deletions

View File

@ -122,7 +122,7 @@ Version numbers correlate directly to the active phase:
- ~~Server-side validation + input sanitization~~ - ~~Server-side validation + input sanitization~~
- ~~Optional Zoho forwarding layer~~ - ~~Optional Zoho forwarding layer~~
- ~~Rate limiting + security headers + CORS~~ - ~~Rate limiting + security headers + CORS~~
- Backend/API hardening as needed - ~~Backend/API hardening as needed~~ ✅
- **Phase 5 — Verification + Release Readiness**: `0.5.x` - **Phase 5 — Verification + Release Readiness**: `0.5.x`
- Build/runtime verification - Build/runtime verification

View File

@ -1,7 +1,7 @@
{ {
"name": "queuenorth-website", "name": "queuenorth-website",
"private": true, "private": true,
"version": "0.4.7", "version": "0.4.8",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently \"vite\" \"node server/index.js\"", "dev": "concurrently \"vite\" \"node server/index.js\"",

View File

@ -121,8 +121,6 @@ app.use((req, res, next) => {
next() next()
}) })
app.use(express.static(path.join(__dirname, '../dist')))
// --- Database --- // --- Database ---
const db = sqlite3(dbPath) const db = sqlite3(dbPath)
@ -300,14 +298,21 @@ async function forwardToZoho(leadData) {
// Health check // Health check
app.get('/api/health', (req, res) => { 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 // Submit lead
app.post('/api/leads', (req, res) => { app.post('/api/leads', (req, res) => {
try { try {
const parsed = leadSchema.safeParse(req.body) const parsed = leadSchema.safeParse(req.body)
if (!parsed.success) { if (!parsed.success) {
const fieldErrors = {} const fieldErrors = {}
for (const issue of parsed.error.issues) { 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) // Fire-and-forget Zoho forwarding (best-effort, non-blocking)
forwardToZoho(sanitized) 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) { } catch (err) {
log.error('Error submitting lead:', err) log.error('Error submitting lead:', err)
res.status(500).json({ error: 'Failed to submit lead' }) 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) => { app.post('/api/support', (req, res) => {
try { try {
const parsed = supportSchema.safeParse(req.body) const parsed = supportSchema.safeParse(req.body)
if (!parsed.success) { if (!parsed.success) {
const fieldErrors = {} const fieldErrors = {}
for (const issue of parsed.error.issues) { 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})`) 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) { } catch (err) {
log.error('Error submitting support request:', err) log.error('Error submitting support request:', err)
res.status(500).json({ error: 'Failed to submit support request' }) 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 --- // --- Start Server ---
const PORT = process.env.SERVER_PORT || 3001 const PORT = process.env.SERVER_PORT || 3001