Fix crash loop: disable keep-alive agents and add unhandledRejection guard

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013AQYLFrscmMnzMiXAAANoa
This commit is contained in:
Claude committed 2026-08-07 00:02:01 +00:00
1 parent b13a8c343f
commit dcfd9e9b65
2 files changed
+19 -10

No files matched your search

+11 -2
View File
@@ -6,8 +6,8 @@ const http = require('http');
const { randomUUID } = require('crypto'); const { randomUUID } = require('crypto');
require('dotenv').config(); require('dotenv').config();
// Prevent HTTP parser errors (e.g. from keep-alive connections to monitored // Prevent HTTP parser errors (e.g. "Data after Connection: close") and other
// services that send data after Connection: close) from crashing the process. // transient async errors from crashing the process.
process.on('uncaughtException', (err) => { process.on('uncaughtException', (err) => {
if (err.code === 'HPE_INVALID_CONSTANT' || (err.message && err.message.includes('Parse Error'))) { if (err.code === 'HPE_INVALID_CONSTANT' || (err.message && err.message.includes('Parse Error'))) {
console.error('HTTP parse error (swallowed):', err.message); console.error('HTTP parse error (swallowed):', err.message);
@@ -17,6 +17,15 @@ process.on('uncaughtException', (err) => {
process.exit(1); 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 db = require('./database');
const scheduler = require('./scheduler'); const scheduler = require('./scheduler');
const monitorService = require('./monitorService'); const monitorService = require('./monitorService');
+8 -8
View File
@@ -31,14 +31,14 @@ function parseHostPort(urlString, fallbackPort) {
class MonitorService { class MonitorService {
constructor() { constructor() {
// Keep-alive agents for pooled HTTP/S connections. // Disable keep-alive to avoid "Parse Error: Data after Connection: close"
// Socket-level 'error' listener prevents parse errors (e.g. "Data after // crashes. Keep-alive connection reuse causes the HTTP parser to throw
// Connection: close") from bubbling up as uncaught exceptions when a // when a monitored server sends extra data after Connection: close — the
// monitored server sends a malformed response on a reused connection. // socket-level error becomes an uncaught exception that kills the process.
this.httpAgent = new http.Agent({ keepAlive: true, maxSockets: 50 }); // Monitoring checks are infrequent enough that connection pooling provides
this.httpAgent.on('error', () => {}); // no meaningful benefit.
this.httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 50 }); this.httpAgent = new http.Agent({ keepAlive: false });
this.httpsAgent.on('error', () => {}); this.httpsAgent = new https.Agent({ keepAlive: false });
// Circuit breaker for HTTP checks (5 failures before opening, 60s reset) // Circuit breaker for HTTP checks (5 failures before opening, 60s reset)
this.httpCircuitBreaker = new CircuitBreaker(5, 60000, this.isTransientError); this.httpCircuitBreaker = new CircuitBreaker(5, 60000, this.isTransientError);