mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
fix(sse): default crash-guard logger to console.warn, not console (#12042)
Real production incident (2026-08-29): the crash guard #11556 introduced defaulted its logger to `log ?? console` — console is an object, not a function, so a burst of client aborts (ECONNRESET) reaching the process-level guard threw TypeError inside the uncaughtException handler itself and killed the server, twice in three minutes. Fix: default to console.warn.bind(console). Bug-injection round trip confirms the new test fails on the old default and passes on the fix. Existing guard suite stays green: 9/9 (verified together with the new test).
This commit is contained in:
@@ -117,7 +117,10 @@ export function installProcessCrashGuard(log) {
|
||||
if (crashGuardInstalled) return;
|
||||
crashGuardInstalled = true;
|
||||
|
||||
const logger = log ?? console;
|
||||
// `console` is an object, not a callable: `log ?? console` followed by
|
||||
// `logger("warn", ...)` throws TypeError and kills the process on the very
|
||||
// abort the guard exists to swallow. Default to console.warn as a function.
|
||||
const logger = typeof log === "function" ? log : console.warn.bind(console);
|
||||
|
||||
process.on("uncaughtException", (err, origin) => {
|
||||
if (shouldSwallowUncaught(err, origin)) {
|
||||
|
||||
33
tests/unit/httpClientAbortGuard-default-logger.test.mjs
Normal file
33
tests/unit/httpClientAbortGuard-default-logger.test.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
|
||||
import assert from "node:assert";
|
||||
import { test } from "node:test";
|
||||
|
||||
// Production calls installProcessCrashGuard() with NO argument in
|
||||
// apiBridgeServer.ts / liveServer.ts / embedWsProxy.ts. The default logger
|
||||
// must be callable; this file runs in its own node:test process, so the
|
||||
// module-level installed flag starts unset and the no-arg path is exercised
|
||||
// for real. Emitting "uncaughtException" through process.emit would trip the
|
||||
// test runner's own listener, so the guard handler is invoked directly.
|
||||
import { installProcessCrashGuard } from "../../src/shared/utils/httpClientAbortGuard.mjs";
|
||||
|
||||
test("installProcessCrashGuard() with no argument swallows a client abort without throwing", () => {
|
||||
const warnings = [];
|
||||
const originalWarn = console.warn;
|
||||
console.warn = (...args) => warnings.push(args);
|
||||
try {
|
||||
installProcessCrashGuard();
|
||||
const handlers = process
|
||||
.listeners("uncaughtException")
|
||||
.filter((fn) => fn.toString().includes("swallowed client-abort"));
|
||||
assert.ok(handlers.length > 0, "guard handler must be registered");
|
||||
const abortErr = Object.assign(new Error("aborted"), { code: "ECONNRESET" });
|
||||
// A broken default logger (console is an object, not a function) throws
|
||||
// TypeError here — that is what took the production process down.
|
||||
assert.doesNotThrow(() => handlers[0](abortErr, "uncaughtException"));
|
||||
assert.equal(warnings.length, 1, "the swallowed abort must be logged once");
|
||||
assert.ok(String(warnings[0][1]).includes("swallowed client-abort"));
|
||||
} finally {
|
||||
console.warn = originalWarn;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user