feat: error handling hardening, 404 catch-all, health check DB test, request timeout, global error handlers (v0.4.8)
This commit is contained in:
parent
7257633d94
commit
c2d5873f08
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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\"",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue