Files
ark-stronghold-protocol/tools/assets/env-proxy.mjs
T
weinibuliu b5c29b5335 feedback7 G-TOOLS: asset downloads honour HTTP(S)_PROXY on Node 22.21+ and 24 (PR #289 by @weinibuliu)
Did not apply the patch. It set a global dispatcher on the npm undici package and rewrote the lockfile. Node's built-in fetch does not use that dispatcher, and node:undici is not importable. fetch() reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY only at startup, and only when that startup had NODE_USE_ENV_PROXY=1 (or --use-env-proxy). That is Node >=22.21.0 and >=24.0.0.

tools/fetch-assets.mjs restarts itself once with NODE_USE_ENV_PROXY=1 before downloading. The default fetch in the downloader and the index cache rejects when a proxy is set but this process cannot use it, including Node 22.0–22.20 and 23, instead of fetching directly. No new dependency. Engines stay >=22.

Tests: node --test test/assets-env-proxy.test.js — 8 pass, 0 fail. node --test test/asset-network.test.js test/assets-diy.test.js test/fetch-assets-shrink.test.js test/assets.test.js — 84 pass, 0 fail. Before this commit: npm run -s lint — 0 errors (135 existing warnings); npm run -s typecheck — ok; npm run -s check:imports — exit 0 (3 pre-existing sim-node notes). docs, version and package tests: 46 pass and 1 fail while env-proxy.mjs was still untracked; after git add, node --test test/package.test.js — 10 pass, 0 fail.
2026-10-08 19:07:49 +08:00

146 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// HTTP(S)_PROXY for the asset downloader, without a new dependency.
//
// Node's built-in fetch reads HTTP_PROXY / HTTPS_PROXY / NO_PROXY only at process
// startup, and only when that startup had NODE_USE_ENV_PROXY=1 or --use-env-proxy.
// fetch() honours this on Node >=22.21.0 and >=24.0.0. The CLI flag on the 24
// line is >=24.5.0, so the restart sets the variable, which 24.0.0 already honours.
// Assigning the variable later does nothing. Node 22.0–22.20 and 23 have no such
// switch; engines stay ">=22", and a proxy there is an error rather than a direct
// download.
//
// Nothing else reaches the built-in fetch: `node:undici` is not a public builtin,
// and setGlobalDispatcher on the npm `undici` package does not affect it. The
// fetch-assets script therefore restarts itself once. Any other caller of the
// default fetch is rejected here while a proxy is configured.
import { spawn } from 'node:child_process';
/** Names Node's env proxy actually reads (both cases). NO_PROXY alone is not a proxy. */
export const PROXY_ENV_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'];
/** Set on the one restart, so a failed hand-off cannot spawn forever. */
export const REEXEC_MARKER = 'SP_ASSETS_ENV_PROXY_REEXEC';
/**
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} env
* @returns {boolean}
*/
export function proxyConfigured(env) {
return PROXY_ENV_KEYS.some((key) => typeof env[key] === 'string' && env[key].trim() !== '');
}
/**
* fetch() routes through HTTP(S)_PROXY when NODE_USE_ENV_PROXY is on at startup.
* @param {string} version process.versions.node
* @returns {boolean}
*/
export function nodeHonoursEnvProxy(version) {
const match = /^v?(\d+)\.(\d+)/.exec(String(version ?? ''));
if (!match) return false;
const major = Number(match[1]);
const minor = Number(match[2]);
if (major === 22) return minor >= 21;
return major >= 24;
}
/**
* True only for a process that was started with the switch. A value written onto
* `process.env` afterwards does not count: Node has already decided.
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} env
* @param {string[]} [execArgv]
* @returns {boolean}
*/
export function startedWithEnvProxy(env, execArgv = []) {
if (env.NODE_USE_ENV_PROXY === '1') return true;
if (execArgv.some((arg) => arg === '--use-env-proxy' || arg === '--use_env_proxy')) return true;
const options = env.NODE_OPTIONS;
return typeof options === 'string' && /(?:^|\s)--use[-_]env[-_]proxy(?=\s|$)/.test(options);
}
// Captured at load. Reading process.env.NODE_USE_ENV_PROXY at fetch time would
// treat a late assignment as if Node had seen it.
const STARTED_WITH_ENV_PROXY = startedWithEnvProxy(process.env, process.execArgv);
/**
* @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
* @param {string} [version]
* @param {boolean} [startedWithFlag] defaults to this process's startup snapshot, not to `env`
* @returns {{ action: 'off' | 'already' | 'reexec' | 'unsupported', reason: string }}
*/
export function envProxyPlan(env = process.env, version = process.versions.node, startedWithFlag = STARTED_WITH_ENV_PROXY) {
if (!proxyConfigured(env)) return { action: 'off', reason: '' };
if (!nodeHonoursEnvProxy(version)) {
return {
action: 'unsupported',
reason: `HTTP(S)_PROXY is set, but Node ${version} does not route fetch() through it. `
+ 'Need Node >=22.21.0 or >=24.0.0, started with NODE_USE_ENV_PROXY=1. Refusing to fetch directly.',
};
}
if (startedWithFlag) return { action: 'already', reason: '' };
return {
action: 'reexec',
reason: 'HTTP(S)_PROXY is set, but this process was not started with NODE_USE_ENV_PROXY=1. '
+ 'Node reads that proxy only at startup (Node >=22.21.0 or >=24.0.0). Refusing to fetch directly.',
};
}
/**
* The built-in fetch, or the caller's own implementation unchanged.
* When a proxy is configured and this process cannot use it, the default fetch
* rejects instead of connecting to the origin.
* @param {typeof fetch} [fetchImpl]
* @returns {typeof fetch}
*/
export function guardDefaultFetch(fetchImpl = globalThis.fetch) {
if (fetchImpl !== globalThis.fetch) return fetchImpl;
return (input, init) => {
const plan = envProxyPlan();
if (plan.action === 'reexec' || plan.action === 'unsupported') return Promise.reject(new Error(plan.reason));
return fetchImpl(input, init);
};
}
/**
* Restart the current script once so Node enables env proxy before any fetch.
* @param {object} [io]
* @param {typeof import('node:child_process').spawn} [io.spawnImpl]
* @param {string} [io.execPath]
* @param {string[]} [io.execArgv]
* @param {string[]} [io.argv]
* @param {NodeJS.ProcessEnv} [io.env]
* @param {(line: string) => void} [io.error]
* @returns {boolean} true when the caller must not continue (restarted, or refused)
*/
export function restartForEnvProxy({
spawnImpl = spawn,
execPath = process.execPath,
execArgv = process.execArgv,
argv = process.argv,
env = process.env,
error = (line) => console.error(line),
} = {}) {
const plan = envProxyPlan(env);
if (plan.action === 'off' || plan.action === 'already') return false;
if (plan.action === 'unsupported') {
error(`[assets] ${plan.reason}`);
process.exitCode = 1;
return true;
}
if (env[REEXEC_MARKER] === '1') {
error('[assets] refused to restart again for HTTP(S)_PROXY');
process.exitCode = 1;
return true;
}
error('[assets] HTTP(S)_PROXY is set; restarting once with NODE_USE_ENV_PROXY=1 so fetch() uses it.');
const child = spawnImpl(execPath, [...execArgv, ...argv.slice(1)], {
env: { ...env, NODE_USE_ENV_PROXY: '1', [REEXEC_MARKER]: '1' },
stdio: 'inherit',
});
child.on('error', (e) => {
error(`[assets] could not restart with NODE_USE_ENV_PROXY: ${e.message}`);
process.exitCode = 1;
});
child.on('close', (code) => { process.exitCode = code ?? 1; });
return true;
}