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']); + }); });