Files
OmniRoute/tests/unit/cli-waitForServer.test.mjs
Diego Rodrigues de Sa e Souza c0682c1ac2 fix(cli): waitForServer must not report ready on bare TCP accept (#6800) (#6892)
waitForServer() polled /api/monitoring/health but fell back to declaring
the server ready once the port had merely accepted TCP connections for
>= 3s, even if no HTTP response was ever received. On CPU-bound warmup
(e.g. small VPS running Next.js standalone), the OS-level listener
accepts TCP almost immediately while the request pipeline is still
compiling, so the fallback fired within ~3-7s and the CLI printed
'OmniRoute is running!' 30-60s before any route actually answered.

Classify each health poll into ready / fast-reject / hanging /
not-listening: only a fast HTTP rejection (fetch error that is not a
timeout, e.g. ECONNRESET before the route mounts) grants the original
#2460 Windows-cold-start grace window. A request that times out with
zero response (the reported #6800 symptom) resets the grace window
instead of accumulating toward it.

Regression tests: tests/unit/waitForServer-tcp-fallback-6800.test.mjs
(new RED-then-GREEN probe from the bug analysis) and
tests/unit/cli-waitForServer.test.mjs (existing suite realigned to the
corrected contract, plus a new case for the hanging-socket scenario).
2026-07-12 01:59:49 -03:00

80 lines
3.0 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import net from "node:net";
import { waitForServer } from "../../bin/cli/utils/pid.mjs";
// #2460 / #6800: waitForServer must (a) respect a 60s default timeout,
// (b) return true when the port is listening for >= 3s and health requests
// are being fast-rejected/reset (route not yet mounted, common on Windows
// during slow Next.js cold start), (c) return false cleanly when nothing is
// listening, and (d) return false when the port merely accepts TCP and then
// hangs without ever answering a request (#6800 — a still-booting/CPU-bound
// process must NOT be reported as ready just because the socket is open).
async function freePort() {
return new Promise((resolve) => {
const server = net.createServer();
server.unref();
server.listen(0, () => {
const { port } = server.address();
server.close(() => resolve(port));
});
});
}
test("waitForServer returns false on a closed port within the given timeout (#2460)", async () => {
const port = await freePort();
const start = Date.now();
const result = await waitForServer(port, 1200);
const elapsed = Date.now() - start;
assert.equal(result, false);
assert.ok(elapsed >= 1200 && elapsed < 4000, `elapsed ${elapsed}ms outside expected range`);
});
test("waitForServer returns true via TCP fallback when health requests are fast-rejected (route not yet mounted) (#2460)", async () => {
const port = await freePort();
const server = net.createServer((socket) => {
// Actively reset the connection quickly — simulates a Node process
// that has bound the port and is responsive, but has not yet mounted
// the health route (the original #2460 Windows cold-start scenario).
socket.destroy();
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, "127.0.0.1", () => resolve());
});
try {
const result = await waitForServer(port, 8000);
assert.equal(result, true, "expected TCP fallback to mark the server ready");
} finally {
await new Promise((resolve) => server.close(() => resolve()));
}
});
test("waitForServer returns false when the port accepts TCP but never answers a request (#6800)", async () => {
const port = await freePort();
const server = net.createServer((socket) => {
// Accept the connection but never respond and never close it — a
// still-booting/CPU-bound process that has bound the port but cannot
// yet process any request. This must NOT be reported as ready.
socket.on("data", () => {});
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, "127.0.0.1", () => resolve());
});
try {
const result = await waitForServer(port, 8000);
assert.equal(
result,
false,
"expected waitForServer to NOT report ready for a TCP-open-but-never-responding socket"
);
} finally {
await new Promise((resolve) => server.close(() => resolve()));
}
});