Merge pull request #8535 from sid-bruno/fix/system-proxy-refresh

fix: system proxy refresh
This commit is contained in:
Bijin A B
2026-07-08 22:04:20 +05:30
committed by GitHub
4 changed files with 141 additions and 6 deletions

View File

@@ -1,10 +1,47 @@
const { getSystemProxy } = require('@usebruno/requests');
const { getSystemProxy, fetchShellEnv } = require('@usebruno/requests');
const PROXY_ENV_KEYS = ['http_proxy', 'HTTP_PROXY', 'https_proxy', 'HTTPS_PROXY', 'no_proxy', 'NO_PROXY', 'all_proxy', 'ALL_PROXY'];
const TIMEOUT_MS = 60_000;
let cachedSystemProxy;
let systemProxyPromise;
const loadSystemProxy = async () => {
// Re-syncs proxy-related process.env values from the user's shell config, without restarting the app.
// ponytail: proxy vars are cleared before spawning the shell (not after fetching) because the child
// process inherits process.env — a removed .zshrc export would otherwise still be echoed back as "current".
const refreshShellEnvProxyVars = async () => {
if (process.platform === 'win32') return {};
const snapshot = {};
for (const key of PROXY_ENV_KEYS) {
snapshot[key] = process.env[key];
delete process.env[key];
}
let timer;
const timeout = new Promise((resolve) => {
timer = setTimeout(() => resolve(null), TIMEOUT_MS);
});
const result = await Promise.race([fetchShellEnv(), timeout]).finally(() => clearTimeout(timer));
if (result === null) {
for (const key of PROXY_ENV_KEYS) {
if (snapshot[key] !== undefined) process.env[key] = snapshot[key];
}
return {};
}
for (const key of PROXY_ENV_KEYS) {
if (result[key]) process.env[key] = result[key];
}
return result;
};
const loadSystemProxy = async ({ refreshShellEnv = false } = {}) => {
try {
if (refreshShellEnv) {
await refreshShellEnvProxyVars();
}
cachedSystemProxy = await getSystemProxy();
} catch (error) {
console.error('Failed to initialize system proxy:', error);
@@ -21,7 +58,7 @@ const loadSystemProxy = async () => {
const fetchSystemProxy = ({ refresh = false } = {}) => {
if (refresh || !systemProxyPromise) {
systemProxyPromise = loadSystemProxy();
systemProxyPromise = loadSystemProxy({ refreshShellEnv: refresh });
}
return systemProxyPromise;
};

View File

@@ -0,0 +1,92 @@
let mockFetchShellEnv;
let mockGetSystemProxy;
jest.mock('@usebruno/requests', () => ({
fetchShellEnv: (...args) => mockFetchShellEnv(...args),
getSystemProxy: (...args) => mockGetSystemProxy(...args)
}));
const PROXY_ENV_KEYS = ['http_proxy', 'HTTP_PROXY', 'https_proxy', 'HTTPS_PROXY', 'no_proxy', 'NO_PROXY', 'all_proxy', 'ALL_PROXY'];
describe('system-proxy refresh', () => {
let fetchSystemProxy;
const originalPlatform = process.platform;
beforeEach(() => {
jest.resetModules();
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
mockFetchShellEnv = jest.fn(() => Promise.resolve({}));
mockGetSystemProxy = jest.fn(() => Promise.resolve({ source: 'environment' }));
for (const key of PROXY_ENV_KEYS) delete process.env[key];
({ fetchSystemProxy } = require('../system-proxy'));
});
it('updates proxy env vars from shell config', async () => {
process.env.http_proxy = 'http://old-proxy:8080';
mockFetchShellEnv = jest.fn(() => Promise.resolve({ http_proxy: 'http://new-proxy:8080' }));
({ fetchSystemProxy } = require('../system-proxy'));
await fetchSystemProxy({ refresh: true });
expect(process.env.http_proxy).toBe('http://new-proxy:8080');
});
it('removes proxy env vars missing from shell config (user removed the export)', async () => {
process.env.http_proxy = 'http://old-proxy:8080';
process.env.no_proxy = 'localhost';
mockFetchShellEnv = jest.fn(() => Promise.resolve({}));
({ fetchSystemProxy } = require('../system-proxy'));
await fetchSystemProxy({ refresh: true });
expect(process.env.http_proxy).toBeUndefined();
expect(process.env.no_proxy).toBeUndefined();
});
it('restores prior proxy vars when the shell fetch fails', async () => {
// fetchShellEnv never rejects - it resolves null on subprocess failure (see shell-env.ts).
process.env.http_proxy = 'http://existing:8080';
mockFetchShellEnv = jest.fn(() => Promise.resolve(null));
({ fetchSystemProxy } = require('../system-proxy'));
await fetchSystemProxy({ refresh: true });
expect(process.env.http_proxy).toBe('http://existing:8080');
});
it('does not touch process.env or invoke the shell on Windows', async () => {
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
process.env.HTTP_PROXY = 'http://launcher-proxy:8080';
mockFetchShellEnv = jest.fn(() => Promise.resolve({ HTTP_PROXY: 'http://should-not-appear:8080' }));
({ fetchSystemProxy } = require('../system-proxy'));
await fetchSystemProxy({ refresh: true });
expect(mockFetchShellEnv).not.toHaveBeenCalled();
expect(process.env.HTTP_PROXY).toBe('http://launcher-proxy:8080');
});
it('does not refresh shell env when refresh is not requested', async () => {
await fetchSystemProxy();
expect(mockFetchShellEnv).not.toHaveBeenCalled();
expect(mockGetSystemProxy).toHaveBeenCalledTimes(1);
});
describe('timeout', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());
it('restores prior proxy vars if the shell fetch hangs past 60s', async () => {
process.env.http_proxy = 'http://existing:8080';
mockFetchShellEnv = jest.fn(() => new Promise(() => {})); // never resolves
({ fetchSystemProxy } = require('../system-proxy'));
const p = fetchSystemProxy({ refresh: true });
jest.advanceTimersByTime(60_000);
await p;
expect(process.env.http_proxy).toBe('http://existing:8080');
});
});
});

View File

@@ -8,7 +8,7 @@ export { transformProxyConfig } from './utils/proxy-util';
export { default as createVaultClient, VaultError } from './utils/node-vault';
export type { VaultClient, VaultConfig, VaultRequestOptions } from './utils/node-vault';
export { getHttpHttpsAgents, resolveAgentsFromPac, PatchedHttpsProxyAgent } from './utils/http-https-agents';
export { initializeShellEnv } from './utils/shell-env';
export { initializeShellEnv, fetchShellEnv } from './utils/shell-env';
export { getOrCreateHttpsAgent, getOrCreateHttpAgent, clearAgentCache, getAgentCacheSize } from './utils/agent-cache';
export { getPacResolver, clearPacCache } from './utils/pac-resolver';
export type { PacWrapper, GetPacResolverParams } from './utils/pac-resolver';

View File

@@ -6,8 +6,9 @@
import path from 'path';
const fetchShellEnv = async (): Promise<Record<string, string>> => {
export const fetchShellEnv = async (): Promise<Record<string, string> | null> => {
// Windows handles environment variables differently - skip
// everything related to windows proxy settings is handled by the system proxy resolver i.e getSystemProxy()
if (process.platform === 'win32') {
return {};
}
@@ -18,7 +19,7 @@ const fetchShellEnv = async (): Promise<Record<string, string>> => {
const env = await shellEnv();
return env;
} catch (error) {
return {};
return null;
}
};
@@ -30,6 +31,11 @@ const fetchShellEnv = async (): Promise<Record<string, string>> => {
*/
export const initializeShellEnv = async (): Promise<Record<string, string>> => {
const shellEnvVars = await fetchShellEnv();
if (shellEnvVars === null) {
return {};
}
for (const [key, value] of Object.entries(shellEnvVars)) {
if (key === 'PATH' && process.env.PATH) {
process.env.PATH = `${value}${path.delimiter}${process.env.PATH}`;