fix(cli): coerce ServerSupervisor exit code to number — prevents TypeError on Node.js v24 (#3748) (#3750)

Node.js v24 added strict type checking to process.exit() and throws
TypeError [ERR_INVALID_ARG_TYPE] when given a non-number. The spawn
'error' event passes err.code (e.g. 'ENOENT') — a string, not a number
— via `err.code ?? -1` (nullish coalescing doesn't help since 'ENOENT'
is not null/undefined). handleExit() now normalises the code to a number
at the top; the 'error' callback passes -1 unconditionally.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-13 01:15:53 -03:00
committed by GitHub
parent 62a0f6a542
commit 33667fcf3a
3 changed files with 45 additions and 4 deletions

View File

@@ -4,6 +4,14 @@
---
## [3.8.24] — TBD
### 🐛 Fixed
- fix(cli): `ServerSupervisor.handleExit` now coerces the exit code to a number before calling `process.exit()` — Node.js v24 throws `TypeError [ERR_INVALID_ARG_TYPE]` when `process.exit()` receives a string (e.g. `'ENOENT'` from a spawn `error` event's `err.code`). The `error` callback also now passes `-1` instead of the raw `err.code`, which is an OS error string rather than a meaningful exit code. ([#3748](https://github.com/diegosouzapw/OmniRoute/issues/3748))
---
## [3.8.23] — TBD
### ✨ New Features

View File

@@ -42,17 +42,20 @@ export class ServerSupervisor {
});
}
this.child.on("error", (err) => this.handleExit(err.code ?? -1, err));
this.child.on("error", (err) => this.handleExit(-1, err));
this.child.on("exit", (code) => this.handleExit(code));
return this.child;
}
handleExit(code) {
// Node.js v24+ requires process.exit() to receive a number. Spawn-error events
// deliver err.code (a string like 'ENOENT') via the 'error' listener; normalise here.
const exitCode = typeof code === "number" ? code : null;
cleanupPidFile("server");
if (this.isShuttingDown || code === 0) {
process.exit(code || 0);
if (this.isShuttingDown || exitCode === 0) {
process.exit(exitCode ?? 0);
return;
}
@@ -71,7 +74,7 @@ export class ServerSupervisor {
}
}
this.dumpCrashLog();
process.exit(code ?? 1);
process.exit(exitCode ?? 1);
return;
}

View File

@@ -195,6 +195,36 @@ test("ServerSupervisor reseta restartCount após processo viver >=30s", async ()
assert.equal(supervisor.restartCount, 1); // reset p/ 0, depois incrementado p/ 1
});
// --- Node.js v24 compat: process.exit() must receive a number (#3748) ---
test("ServerSupervisor.handleExit com string code não passa string para process.exit (#3748)", async () => {
const { ServerSupervisor } = await import("../../bin/cli/runtime/processSupervisor.mjs");
const exits: Array<number | string | undefined> = [];
const origExit = process.exit.bind(process);
// @ts-ignore
process.exit = (code?: number | string) => exits.push(code);
const supervisor = new ServerSupervisor({
serverPath: "/fake/server.js",
env: {},
maxRestarts: 0,
});
// Simulates the 'error' event on child spawn failure: err.code = 'ENOENT' (string, not number).
// maxRestarts=0 → restartCount(0) >= maxRestarts(0) → process.exit() is called immediately.
supervisor.startedAt = Date.now() - 100;
supervisor.handleExit("ENOENT" as any);
// @ts-ignore
process.exit = origExit;
assert.equal(exits.length, 1, "process.exit deve ser chamado exatamente 1 vez");
assert.equal(
typeof exits[0],
"number",
`process.exit deve receber number, recebeu: ${typeof exits[0]} (${exits[0]})`
);
});
// --- pid.mjs multi-service ---
test("writePidFile/readPidFile/cleanupPidFile operam por service", async () => {