fix(cli): probe both IPv4 and IPv6 loopback for server readiness (#11766) (#11794)

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:
vermasomesh835
2026-08-30 17:39:16 +05:30
committed by GitHub
parent 131e413cbd
commit b07eaafcc4
2 changed files with 87 additions and 22 deletions

View File

@@ -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);
}

View File

@@ -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()));
}
});