diff --git a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
index 9cdb53987..e7931f2d7 100644
--- a/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
+++ b/packages/bruno-app/src/components/Preferences/ProxySettings/index.js
@@ -3,11 +3,11 @@ import { useFormik } from 'formik';
import * as Yup from 'yup';
import debounce from 'lodash/debounce';
import toast from 'react-hot-toast';
-import { savePreferences } from 'providers/ReduxStore/slices/app';
+import { savePreferences, refreshPacCache } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
import { useDispatch, useSelector } from 'react-redux';
-import { IconEye, IconEyeOff } from '@tabler/icons';
+import { IconEye, IconEyeOff, IconRefresh } from '@tabler/icons';
import { useState } from 'react';
import SystemProxy from './SystemProxy';
@@ -103,6 +103,12 @@ const ProxySettings = ({ close }) => {
[]
);
+ const handleRefreshPac = () => {
+ dispatch(refreshPacCache())
+ .then(() => toast.success('PAC cache refreshed'))
+ .catch(() => toast.error('Failed to refresh PAC cache'));
+ };
+
const [passwordVisible, setPasswordVisible] = useState(false);
const [proxyMode, setProxyMode] = useState(() => {
if (preferences.proxy.disabled) return 'off';
@@ -451,6 +457,15 @@ const ProxySettings = ({ close }) => {
? 'Enter the URL to your PAC file'
: 'Supports .pac files for automatic proxy configuration'}
+ {formik.values.pac.source ? (
+
+
+ Refetch
+
+ ) : null}
>
) : null}
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
index 69abb02ef..2de211e42 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js
@@ -378,4 +378,11 @@ export const clearHttpHttpsAgentCache = () => () => {
});
};
+export const refreshPacCache = () => () => {
+ return new Promise((resolve, reject) => {
+ const { ipcRenderer } = window;
+ ipcRenderer.invoke('renderer:refresh-pac-cache').then(resolve).catch(reject);
+ });
+};
+
export default appSlice.reducer;
diff --git a/packages/bruno-electron/src/ipc/preferences.js b/packages/bruno-electron/src/ipc/preferences.js
index 64f28c598..89807ca1c 100644
--- a/packages/bruno-electron/src/ipc/preferences.js
+++ b/packages/bruno-electron/src/ipc/preferences.js
@@ -8,7 +8,7 @@ const { resolveDefaultLocation } = require('../utils/default-location');
const onboardUser = require('../app/onboarding');
const LastOpenedCollections = require('../store/last-opened-collections');
const WindowStateStore = require('../store/window-state');
-const { clearAgentCache } = require('@usebruno/requests');
+const { clearAgentCache, clearPacCache } = require('@usebruno/requests');
const registerPreferencesIpc = (mainWindow) => {
const lastOpenedCollections = new LastOpenedCollections();
@@ -67,6 +67,15 @@ const registerPreferencesIpc = (mainWindow) => {
}
});
+ ipcMain.handle('renderer:refresh-pac-cache', async () => {
+ try {
+ clearPacCache();
+ clearAgentCache();
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
ipcMain.on('renderer:theme-change', (event, theme, themeBg) => {
nativeTheme.themeSource = theme;
const windowStateStore = new WindowStateStore();
@@ -81,7 +90,10 @@ const registerPreferencesIpc = (mainWindow) => {
});
ipcMain.handle('renderer:refresh-system-proxy', async () => {
- return await fetchSystemProxy({ refresh: true });
+ const variables = await fetchSystemProxy({ refresh: true });
+ clearPacCache();
+ clearAgentCache();
+ return variables;
});
};
diff --git a/packages/bruno-requests/src/utils/pac-resolver.spec.ts b/packages/bruno-requests/src/utils/pac-resolver.spec.ts
index 46689b13e..0f97f383b 100644
--- a/packages/bruno-requests/src/utils/pac-resolver.spec.ts
+++ b/packages/bruno-requests/src/utils/pac-resolver.spec.ts
@@ -278,4 +278,38 @@ describe('pac-resolver (shared)', () => {
clearPacCache();
expect(_CACHE.size).toBe(0);
});
+
+ test('clearPacCache forces a re-read of updated PAC file content on next resolve', async () => {
+ const scriptV1 = 'function FindProxyForURL() { return "PROXY a.example:8080"; }';
+ const scriptV2 = 'function FindProxyForURL() { return "PROXY b.example:9090"; }';
+ const readFileMock = jest.fn().mockResolvedValueOnce(scriptV1).mockResolvedValueOnce(scriptV2);
+ jest.doMock('fs/promises', () => ({ readFile: readFileMock }));
+ jest.doMock('url', () => ({ fileURLToPath: jest.fn(() => '/Users/test/proxy.pac') }));
+ // resolver returns directives based on the exact script it was compiled from
+ jest.doMock('pac-resolver', () => ({
+ createPacResolver: jest.fn((_qjs: any, script: string) =>
+ async () => (script === scriptV1 ? 'PROXY a.example:8080' : 'PROXY b.example:9090')
+ )
+ }));
+ jest.doMock('quickjs-emscripten', () => ({ getQuickJS: jest.fn(async () => ({})) }));
+
+ const { getPacResolver, clearPacCache } = require('./pac-resolver');
+ const pacSource = 'file:///Users/test/proxy.pac';
+
+ const w1 = await getPacResolver({ pacSource });
+ expect(await w1.resolve('http://foo.example/')).toEqual(['PROXY a.example:8080']);
+ expect(readFileMock).toHaveBeenCalledTimes(1);
+
+ // Without refresh, the cached (stale) content is reused — the file is NOT re-read.
+ const wCached = await getPacResolver({ pacSource });
+ expect(wCached).toBe(w1);
+ expect(readFileMock).toHaveBeenCalledTimes(1);
+
+ // Refresh clears the cache, so the edited file is re-read and new directives take effect.
+ clearPacCache();
+ const w2 = await getPacResolver({ pacSource });
+ expect(w2).not.toBe(w1);
+ expect(readFileMock).toHaveBeenCalledTimes(2);
+ expect(await w2.resolve('http://foo.example/')).toEqual(['PROXY b.example:9090']);
+ });
});