mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
Fix correto — CLI agora sonda IPv4 e IPv6 no probe de prontidão do servidor, com teste de regressão próprio (`tests/unit/cli-waitForServer.test.mjs`). Validado no worktree combinado (typecheck limpo, teste focado verde). Obrigado!
This commit is contained in:
@@ -100,31 +100,66 @@ export async function waitForServer(port, timeout = 60000) {
|
||||
// - "hanging": the request timed out waiting for any response — the
|
||||
// process accepted the TCP connection but never answered (#6800).
|
||||
// - "not-listening": nothing is accepting connections on the port at all.
|
||||
// #11766: probe both IPv4 and IPv6 loopback to handle servers listening on
|
||||
// either family (or both).
|
||||
async function pollHealthOnce(port) {
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return res.ok ? "ready" : "fast-reject";
|
||||
} catch (err) {
|
||||
if (err?.name === "TimeoutError") return "hanging";
|
||||
const listening = await isPortListening(port).catch(() => false);
|
||||
return listening ? "fast-reject" : "not-listening";
|
||||
}
|
||||
const hosts = ["127.0.0.1", "::1"];
|
||||
const outcomes = [];
|
||||
|
||||
// Probe both loopback families concurrently
|
||||
const results = await Promise.all(
|
||||
hosts.map(async (host) => {
|
||||
try {
|
||||
const res = await fetch(`http://${host}:${port}/api/monitoring/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return { host, outcome: res.ok ? "ready" : "fast-reject" };
|
||||
} catch (err) {
|
||||
const outcome = err?.name === "TimeoutError" ? "hanging" : "error";
|
||||
return { host, outcome };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
outcomes.push(...results.map((r) => r.outcome));
|
||||
|
||||
// If either family is ready, the server is ready
|
||||
if (outcomes.includes("ready")) return "ready";
|
||||
|
||||
// If either family is fast-reject, treat as fast-reject
|
||||
// (TCP is listening and rejecting, just route not ready yet)
|
||||
if (outcomes.includes("fast-reject")) return "fast-reject";
|
||||
|
||||
// If either family is hanging, server accepted TCP but not answering
|
||||
// (still booting, must not report as ready per #6800)
|
||||
if (outcomes.includes("hanging")) return "hanging";
|
||||
|
||||
// Both families failed — check if either port is actually listening
|
||||
// If listening, then errors above are route-level (fast-reject case)
|
||||
const listening = await isPortListening(port).catch(() => false);
|
||||
return listening ? "fast-reject" : "not-listening";
|
||||
}
|
||||
|
||||
async function isPortListening(port) {
|
||||
const net = await import("node:net");
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect({ host: "127.0.0.1", port, timeout: 1000 });
|
||||
const finish = (ok) => {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
resolve(ok);
|
||||
};
|
||||
socket.once("connect", () => finish(true));
|
||||
socket.once("error", () => finish(false));
|
||||
socket.once("timeout", () => finish(false));
|
||||
});
|
||||
// #11766: check both IPv4 and IPv6 loopback. Return true if either is listening.
|
||||
const hosts = ["127.0.0.1", "::1"];
|
||||
const results = await Promise.all(
|
||||
hosts.map(
|
||||
(host) =>
|
||||
new Promise((resolve) => {
|
||||
const socket = net.connect({ host, port, timeout: 1000 });
|
||||
const finish = (ok) => {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
resolve(ok);
|
||||
};
|
||||
socket.once("connect", () => finish(true));
|
||||
socket.once("error", () => finish(false));
|
||||
socket.once("timeout", () => finish(false));
|
||||
})
|
||||
)
|
||||
);
|
||||
return results.some((ok) => ok);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { waitForServer } from "../../bin/cli/utils/pid.mjs";
|
||||
// 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).
|
||||
// #11766: also test that IPv6 loopback is checked when IPv4 is unavailable.
|
||||
|
||||
async function freePort() {
|
||||
return new Promise((resolve) => {
|
||||
@@ -77,3 +78,32 @@ test("waitForServer returns false when the port accepts TCP but never answers a
|
||||
await new Promise((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
test("waitForServer detects IPv6 loopback health endpoint when IPv4 is unavailable (#11766)", async () => {
|
||||
const port = await freePort();
|
||||
const server = net.createServer((socket) => {
|
||||
socket.on("data", (data) => {
|
||||
const request = data.toString();
|
||||
if (request.includes("GET /api/monitoring/health")) {
|
||||
socket.end("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Listen only on IPv6 loopback
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(port, "::1", () => resolve());
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await waitForServer(port, 8000);
|
||||
assert.equal(
|
||||
result,
|
||||
true,
|
||||
"expected waitForServer to detect health endpoint on IPv6 loopback even when IPv4 is unavailable"
|
||||
);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user