mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
Merge pull request #8535 from sid-bruno/fix/system-proxy-refresh
fix: system proxy refresh
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
92
packages/bruno-electron/src/store/tests/system-proxy.spec.js
Normal file
92
packages/bruno-electron/src/store/tests/system-proxy.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user