From 55120bea08b864a370e4247a4c45e17c5842ebf4 Mon Sep 17 00:00:00 2001 From: Abdul Azeem Date: Sat, 7 Feb 2026 10:30:07 +0300 Subject: [PATCH 1/3] feat: Improves backend stability and data handling Adds error handling for loading .env files to prevent crashes. Implements a size limit for HTTP response bodies to prevent potential memory exhaustion. Adds try-catch blocks for database operations to prevent unhandled exceptions and improves shutdown process to gracefully close the database connection. Adds graceful shutdown on SIGTERM and SIGINT signals. --- backend-server.js | 124 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 93 insertions(+), 31 deletions(-) diff --git a/backend-server.js b/backend-server.js index 2211030..1e02095 100644 --- a/backend-server.js +++ b/backend-server.js @@ -20,7 +20,11 @@ function loadEnvFile(filePath) { process.env[key] = value; } } - } catch (error) {} + } catch (error) { + process.stderr.write( + `Warning: Failed to load .env file: ${error && error.message ? error.message : String(error)}\n` + ); + } } loadEnvFile(path.join(__dirname, ".env")); @@ -98,9 +102,19 @@ function httpJsonGet(targetUrl, timeoutMs) { method: "GET", timeout: timeoutMs, }; + const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB limit const req = transport.request(options, (res) => { const chunks = []; + let totalSize = 0; res.on("data", (chunk) => { + if (settled) return; + totalSize += chunk.length; + if (totalSize > MAX_BODY_SIZE) { + settled = true; + req.destroy(new Error("Response body exceeds maximum size")); + reject(new Error("Response body exceeds maximum size")); + return; + } chunks.push(chunk); }); res.on("end", () => { @@ -424,39 +438,55 @@ async function refreshSnapshot() { const results = await Promise.all(SERVICES.map((service) => probeService(service))); const indicator = computeIndicator(results); if (db) { - const stmt = db.prepare( - "INSERT INTO service_history (service_id, state, latency_ms, checked_at) VALUES (?, ?, ?, ?)" - ); - for (const service of results) { - const checkedAt = Date.parse(service.lastCheckedAt); - const pct = - typeof service.healthPct === "number" ? service.healthPct : null; - let historyState = "up"; - if (pct == null || Number.isNaN(pct)) { - historyState = service.state || "up"; - } else if (pct < 10) { - historyState = "down"; - } else if (pct < 50) { - historyState = "issue"; - } else if (pct < 95) { - historyState = "slow"; - } else { - historyState = "up"; + try { + const stmt = db.prepare( + "INSERT INTO service_history (service_id, state, latency_ms, checked_at) VALUES (?, ?, ?, ?)" + ); + try { + for (const service of results) { + const checkedAt = Date.parse(service.lastCheckedAt); + const pct = + typeof service.healthPct === "number" ? service.healthPct : null; + let historyState = "up"; + if (pct == null || Number.isNaN(pct)) { + historyState = service.state || "up"; + } else if (pct < 10) { + historyState = "down"; + } else if (pct < 50) { + historyState = "issue"; + } else if (pct < 95) { + historyState = "slow"; + } else { + historyState = "up"; + } + stmt.run( + service.id, + historyState, + service.latencyMs, + Number.isNaN(checkedAt) ? Date.now() : checkedAt + ); + } + } finally { + stmt.finalize(); } - stmt.run( - service.id, - historyState, - service.latencyMs, - Number.isNaN(checkedAt) ? Date.now() : checkedAt + const retentionMs = 7 * 24 * 60 * 60 * 1000; + const cutoff = Date.now() - retentionMs; + db.run( + "DELETE FROM service_history WHERE checked_at < ?", + [cutoff], + (err) => { + if (err) { + process.stdout.write( + `Warning: Failed to clean old history: ${err.message}\n` + ); + } + } + ); + } catch (error) { + process.stdout.write( + `Warning: Failed to save history: ${error && error.message ? error.message : String(error)}\n` ); } - stmt.finalize(); - const retentionMs = 7 * 24 * 60 * 60 * 1000; - const cutoff = Date.now() - retentionMs; - db.run( - "DELETE FROM service_history WHERE checked_at < ?", - [cutoff] - ); } process.stdout.write( `Snapshot refreshed: ${results.length} services, indicator=${indicator.indicator}\n` @@ -568,3 +598,35 @@ const server = http.createServer((req, res) => { server.listen(PORT, () => { process.stdout.write(`Status backend listening on http://localhost:${PORT}\n`); }); + +function gracefulShutdown() { + process.stdout.write("Shutting down gracefully...\n"); + server.close(() => { + process.stdout.write("HTTP server closed\n"); + if (db) { + db.close((err) => { + if (err) { + process.stderr.write(`Error closing database: ${err.message}\n`); + process.exit(1); + } else { + process.stdout.write("Database closed\n"); + process.exit(0); + } + }); + } else { + process.exit(0); + } + }); + + // Force shutdown after 10 seconds + setTimeout(() => { + process.stderr.write("Forced shutdown after timeout\n"); + if (db) { + db.close(() => {}); + } + process.exit(1); + }, 10000); +} + +process.on("SIGTERM", gracefulShutdown); +process.on("SIGINT", gracefulShutdown); From 2eff656c583127e58d3b1a7c4bbdc9678d47bf98 Mon Sep 17 00:00:00 2001 From: Abdul Azeem Date: Sat, 7 Feb 2026 10:34:37 +0300 Subject: [PATCH 2/3] chore: formatting code to match standards --- backend-server.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/backend-server.js b/backend-server.js index 1e02095..d67f2fb 100644 --- a/backend-server.js +++ b/backend-server.js @@ -45,8 +45,7 @@ try { } catch (error) { db = null; process.stdout.write( - `SQLite history database disabled: ${ - error && error.message ? error.message : String(error) + `SQLite history database disabled: ${error && error.message ? error.message : String(error) }\n` ); } @@ -70,8 +69,8 @@ try { ? service.network : typeof service.environment === "string" && service.environment.length > 0 - ? service.environment - : null; + ? service.environment + : null; return { id: service.id, name: service.name, @@ -225,8 +224,8 @@ async function probeService(service) { service.checker === "statusEquals" ? checkByStatusEquals(service, payload) : service.checker === "cycleFreshSeconds" - ? checkByCycleFresh(service, payload) - : { ok: false, reason: "Unknown checker configuration" }; + ? checkByCycleFresh(service, payload) + : { ok: false, reason: "Unknown checker configuration" }; let state = "operational"; let detail = baseCheck.reason || null; let healthPct = 100; @@ -278,10 +277,10 @@ function computeIndicator(services) { typeof service.healthPct === "number" ? service.healthPct : service.state === "operational" - ? 100 - : service.state === "degraded" - ? 20 - : 0; + ? 100 + : service.state === "degraded" + ? 20 + : 0; if (pct === 0) { outageCount += 1; } else if (pct < 100) { @@ -617,12 +616,12 @@ function gracefulShutdown() { process.exit(0); } }); - + // Force shutdown after 10 seconds setTimeout(() => { process.stderr.write("Forced shutdown after timeout\n"); if (db) { - db.close(() => {}); + db.close(() => { }); } process.exit(1); }, 10000); From 79f682b3dec983bec0bbdc88a7f576219b855731 Mon Sep 17 00:00:00 2001 From: Abdul Azeem Date: Sun, 8 Feb 2026 14:56:03 +0300 Subject: [PATCH 3/3] feat: add config validation and improve code readability Enhances configuration loading with thorough validation. It checks for valid port numbers and service definitions within the `services.json` file, ensuring each service has a unique ID, a valid URL, and other required properties. This prevents common startup errors due to malformed configurations and guides users to fix issues quickly. Also makes the error reporting more user-friendly by displaying the exact configuration error instead of crashing silently. refactor: move urlCheck to validation --- backend-server.js | 151 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 26 deletions(-) diff --git a/backend-server.js b/backend-server.js index d67f2fb..a1d7ac1 100644 --- a/backend-server.js +++ b/backend-server.js @@ -50,42 +50,141 @@ try { ); } -const PORT = process.env.PORT || 6969; +function validateRuntimeConfig(rawPort, services) { + const response = { success: false, reason: "" }; + const isNonEmptyString = (value) => + typeof value === "string" && value.trim().length > 0; + const numericPort = Number(rawPort); + if (!Number.isInteger(numericPort)) { + response.reason = 'config "PORT" must be an integer'; + return response; + } + if (numericPort < 1 || numericPort > 65535) { + response.reason = 'config "PORT" must be between 1 and 65535'; + return response; + } + if (!Array.isArray(services)) { + response.reason = 'services.json must contain an array'; + return response; + } + const seenIds = new Set(); + for (let i = 0; i < services.length; i += 1) { + const service = services[i]; + if (!service || typeof service !== "object") { + response.reason = `service[${i}] must be an object`; + return response; + } + if (!isNonEmptyString(service.id)) { + response.reason = `service[${i}] "id" must be a non-empty string`; + return response; + } + if (seenIds.has(service.id)) { + response.reason = `service[${i}] "id" must be unique`; + return response; + } + if (!isNonEmptyString(service.name)) { + response.reason = `service[${i}] "name" must be a non-empty string`; + return response; + } + if (!isNonEmptyString(service.checker)) { + response.reason = `service[${i}] "checker" must be a non-empty string`; + return response; + } + if ( + service.checker !== "statusEquals" && + service.checker !== "cycleFreshSeconds" + ) { + response.reason = `service[${i}] "checker" must be "statusEquals" or "cycleFreshSeconds"`; + return response; + } + if (service.checker === "statusEquals") { + if (!isNonEmptyString(service.expectedStatus)) { + response.reason = `service[${i}] "expectedStatus" must be a non-empty string when checker is "statusEquals"`; + return response; + } + } + if (service.checker === "cycleFreshSeconds") { + if ( + typeof service.maxAgeSeconds !== "number" || + !Number.isFinite(service.maxAgeSeconds) || + service.maxAgeSeconds <= 0 + ) { + response.reason = `service[${i}] "maxAgeSeconds" must be a number > 0 when checker is "cycleFreshSeconds"`; + return response; + } + } + if (service.urlEnv != null && !isNonEmptyString(service.urlEnv)) { + response.reason = `service[${i}] "urlEnv" must be a non-empty string when provided`; + return response; + } + if (service.url != null && !isNonEmptyString(service.url)) { + response.reason = `service[${i}] "url" must be a non-empty string when provided`; + return response; + } + const resolvedUrl = + service.urlEnv && typeof service.urlEnv === "string" + ? process.env[service.urlEnv] + : undefined; + const effectiveUrl = resolvedUrl || service.url; + if (!isNonEmptyString(effectiveUrl)) { + response.reason = `service[${i}] must define "url" or a valid "urlEnv"`; + return response; + } + try { + new URL(effectiveUrl); + } catch (error) { + response.reason = `service[${i}] URL is invalid`; + return response; + } + seenIds.add(service.id); + } + response.success = true; + return response; +} + +const rawPort = process.env.PORT || 6969; +const PORT = Number(rawPort); let SERVICES = []; try { const servicesConfigPath = path.join(__dirname, "services.json"); const raw = fs.readFileSync(servicesConfigPath, "utf8"); const parsed = JSON.parse(raw); - if (Array.isArray(parsed)) { - SERVICES = parsed.map((service) => { - const urlFromEnv = - service.urlEnv && typeof service.urlEnv === "string" - ? process.env[service.urlEnv] - : undefined; - const url = urlFromEnv || service.url || null; - const network = - typeof service.network === "string" && service.network.length > 0 - ? service.network - : typeof service.environment === "string" && - service.environment.length > 0 - ? service.environment - : null; - return { - id: service.id, - name: service.name, - network, - environment: network, - group: service.group, - url, - checker: service.checker, - expectedStatus: service.expectedStatus, - maxAgeSeconds: service.maxAgeSeconds, - }; + const validation = validateRuntimeConfig(rawPort, parsed); + if (!validation.success) { + throw new Error(validation.reason); + } + SERVICES = []; + for (const service of parsed) { + const urlFromEnv = service.urlEnv ? process.env[service.urlEnv] : undefined; + const url = urlFromEnv || service.url || null; + let network = null; + if (typeof service.network === "string" && service.network.length > 0) { + network = service.network; + } else if ( + typeof service.environment === "string" && + service.environment.length > 0 + ) { + network = service.environment; + } + SERVICES.push({ + id: service.id, + name: service.name, + network, + environment: network, + group: service.group, + url, + checker: service.checker, + expectedStatus: service.expectedStatus, + maxAgeSeconds: service.maxAgeSeconds, }); } } catch (error) { SERVICES = []; + process.stderr.write( + `Invalid configuration: ${error && error.message ? error.message : String(error)}\n` + ); + process.exit(1); } function httpJsonGet(targetUrl, timeoutMs) {