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~~ ✅
|
- ~~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
|
||||||
|
|
|
||||||
|
|
@ -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\"",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue