From dcfd9e9b656a541651b54de42cb7f03c6fa8080b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 00:02:01 +0000 Subject: [PATCH] Fix crash loop: disable keep-alive agents and add unhandledRejection guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix (agent .on('error')) doesn't reach individual socket objects — socket-level parse errors still escaped as uncaught exceptions. Root fix: disable keepAlive on the HTTP/HTTPS monitoring agents. Without connection reuse there is no stale connection to trigger "Parse Error: Data after Connection: close". Monitoring intervals are 60s+ so the lack of connection pooling has no meaningful performance impact. Belt-and-suspenders: add process.on('unhandledRejection') alongside the existing uncaughtException guard to catch any async paths that leak. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_013AQYLFrscmMnzMiXAAANoa --- server/index.js | 13 +++++++++++-- server/monitorService.js | 16 ++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/server/index.js b/server/index.js index bf0e504..21442c1 100644 --- a/server/index.js +++ b/server/index.js @@ -6,8 +6,8 @@ const http = require('http'); const { randomUUID } = require('crypto'); require('dotenv').config(); -// Prevent HTTP parser errors (e.g. from keep-alive connections to monitored -// services that send data after Connection: close) from crashing the process. +// Prevent HTTP parser errors (e.g. "Data after Connection: close") and other +// transient async errors from crashing the process. process.on('uncaughtException', (err) => { if (err.code === 'HPE_INVALID_CONSTANT' || (err.message && err.message.includes('Parse Error'))) { console.error('HTTP parse error (swallowed):', err.message); @@ -17,6 +17,15 @@ process.on('uncaughtException', (err) => { process.exit(1); }); +process.on('unhandledRejection', (reason) => { + const msg = reason instanceof Error ? reason.message : String(reason); + if (msg.includes('Parse Error')) { + console.error('HTTP parse error (swallowed):', msg); + return; + } + console.error('Unhandled rejection:', reason); +}); + const db = require('./database'); const scheduler = require('./scheduler'); const monitorService = require('./monitorService'); diff --git a/server/monitorService.js b/server/monitorService.js index ec044a5..f840a1f 100644 --- a/server/monitorService.js +++ b/server/monitorService.js @@ -31,14 +31,14 @@ function parseHostPort(urlString, fallbackPort) { class MonitorService { constructor() { - // Keep-alive agents for pooled HTTP/S connections. - // Socket-level 'error' listener prevents parse errors (e.g. "Data after - // Connection: close") from bubbling up as uncaught exceptions when a - // monitored server sends a malformed response on a reused connection. - this.httpAgent = new http.Agent({ keepAlive: true, maxSockets: 50 }); - this.httpAgent.on('error', () => {}); - this.httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 50 }); - this.httpsAgent.on('error', () => {}); + // Disable keep-alive to avoid "Parse Error: Data after Connection: close" + // crashes. Keep-alive connection reuse causes the HTTP parser to throw + // when a monitored server sends extra data after Connection: close — the + // socket-level error becomes an uncaught exception that kills the process. + // Monitoring checks are infrequent enough that connection pooling provides + // no meaningful benefit. + this.httpAgent = new http.Agent({ keepAlive: false }); + this.httpsAgent = new https.Agent({ keepAlive: false }); // Circuit breaker for HTTP checks (5 failures before opening, 60s reset) this.httpCircuitBreaker = new CircuitBreaker(5, 60000, this.isTransientError);