fix(cli): prefer IPv4 DNS for spawned servers (#9209)

Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-05 22:40:06 -03:00
committed by GitHub
parent e317385ba8
commit 103bab99d5
5 changed files with 103 additions and 6 deletions

View File

@@ -16,7 +16,7 @@ import {
resolveMaxOldSpaceMb,
calibrateHeapFallbackMb,
buildServerNodeOptions,
buildNodeHeapArgs,
buildNodeRuntimeArgs,
} from "../../../scripts/build/runtime-env.mjs";
import { resolveTlsOptions } from "../../../scripts/dev/tls-options.mjs";
@@ -269,7 +269,7 @@ function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
const server = spawn(
process.versions.bun ? process.execPath : "node",
[...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)), serverJs],
process.versions.bun ? [serverJs] : buildNodeRuntimeArgs(process.env, memoryLimit, serverJs),
{
cwd: APP_DIR,
env,
@@ -289,7 +289,7 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort,
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
const server = spawn(
process.versions.bun ? process.execPath : "node",
[...(process.versions.bun ? [] : buildNodeHeapArgs(process.env, memoryLimit)), serverJs],
process.versions.bun ? [serverJs] : buildNodeRuntimeArgs(process.env, memoryLimit, serverJs),
{
cwd: APP_DIR,
env,

View File

@@ -8,7 +8,7 @@ import {
computeRestartDelayMs,
waitUntilPortFree,
} from "./supervisorPolicy.mjs";
import { buildNodeHeapArgs } from "../../../scripts/build/runtime-env.mjs";
import { buildNodeRuntimeArgs } from "../../../scripts/build/runtime-env.mjs";
import { stopProcessGracefully } from "../../../src/shared/platform/windowsProcess.ts";
import {
isFatalInstrumentationHookFailure,
@@ -47,7 +47,6 @@ export class ServerSupervisor {
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value). The
// calibrated heap is already carried by env.NODE_OPTIONS either way.
const heapArgs = buildNodeHeapArgs(process.env, this.memoryLimit);
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
// wasn't set (the default) — any debug/pino output written to stdout vanished
// silently, so a boot that never becomes ready looked like a dead hang with zero
@@ -55,7 +54,9 @@ export class ServerSupervisor {
// stderr so a readiness timeout can surface what the child actually printed.
this.child = spawn(
process.versions.bun ? process.execPath : "node",
[...(process.versions.bun ? [] : heapArgs), this.serverPath],
process.versions.bun
? [this.serverPath]
: buildNodeRuntimeArgs(process.env, this.memoryLimit, this.serverPath),
{
cwd: dirname(this.serverPath),
env: this.env,

View File

@@ -0,0 +1 @@
- **fix(cli):** prefer IPv4 DNS for spawned Node servers. (thanks @dsitmilis)

View File

@@ -86,6 +86,20 @@ export function buildNodeHeapArgs(env = process.env, memoryLimit) {
return envHasExplicitHeapFlag(env) ? [] : [`${MAX_OLD_SPACE_FLAG}=${memoryLimit}`];
}
/**
* Build the complete argument list for spawning the Node.js server runtime.
* Prefer IPv4 DNS results before starting the application so undici does not
* stall on hosts whose IPv6 route silently drops outbound connections.
*
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
* @param {number} memoryLimit — calibrated V8 heap ceiling (MB)
* @param {string} serverPath — standalone server entrypoint
* @returns {string[]}
*/
export function buildNodeRuntimeArgs(env = process.env, memoryLimit, serverPath) {
return ["--dns-result-order=ipv4first", ...buildNodeHeapArgs(env, memoryLimit), serverPath];
}
/**
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [fromEnv]
* Defaults to process.env. Pass bootstrap `merged` so project `.env` PORT applies before spawn.

View File

@@ -0,0 +1,81 @@
import test from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { mkdtempSync, rmSync } from "node:fs";
import { createRequire, syncBuiltinESMExports } from "node:module";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { buildNodeRuntimeArgs } from "../../scripts/build/runtime-env.mjs";
const require = createRequire(import.meta.url);
const childProcess = require("node:child_process");
const originalSpawn = childProcess.spawn;
test.afterEach(() => {
childProcess.spawn = originalSpawn;
syncBuiltinESMExports();
});
test("Node server runtime prefers IPv4 DNS before the server entrypoint", () => {
assert.deepEqual(buildNodeRuntimeArgs({}, 2048, "/app/server.js"), [
"--dns-result-order=ipv4first",
"--max-old-space-size=2048",
"/app/server.js",
]);
});
test("Node server runtime preserves an explicit heap setting while preferring IPv4", () => {
assert.deepEqual(
buildNodeRuntimeArgs(
{ NODE_OPTIONS: "--enable-source-maps --max-old-space-size=8192" },
512,
"/app/server.js"
),
["--dns-result-order=ipv4first", "/app/server.js"]
);
});
test("ServerSupervisor starts Node with IPv4-first DNS", async () => {
const spawnCalls: Array<{ command: string; args: string[] }> = [];
const child = Object.assign(new EventEmitter(), {
pid: 2699,
stdout: new EventEmitter(),
stderr: new EventEmitter(),
});
childProcess.spawn = (command: string, args: string[]) => {
spawnCalls.push({ command, args });
return child;
};
syncBuiltinESMExports();
const dataDir = mkdtempSync(join(tmpdir(), "omniroute-ipv4-first-"));
const previousDataDir = process.env.DATA_DIR;
process.env.DATA_DIR = dataDir;
try {
const moduleUrl = pathToFileURL(
join(process.cwd(), "bin/cli/runtime/processSupervisor.mjs")
).href;
const { ServerSupervisor } = await import(`${moduleUrl}?ipv4-first=${Date.now()}`);
const supervisor = new ServerSupervisor({
serverPath: "/app/server.js",
env: {},
memoryLimit: 2048,
});
supervisor.start();
assert.deepEqual(spawnCalls, [
{
command: "node",
args: ["--dns-result-order=ipv4first", "--max-old-space-size=2048", "/app/server.js"],
},
]);
} finally {
if (previousDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = previousDataDir;
rmSync(dataDir, { recursive: true, force: true });
}
});