fix(desktop): restore saved outbound proxy settings on app start (#949)
The standalone server hydrates its proxy state in index.ts right after initDb, but the desktop embedder (desktop/src/server-host.ts) builds the app without index.ts and never did — so a proxy URL saved through PUT /api/settings/proxy sat in the settings table while the process started with an empty proxy, and every restart made the Outbound proxy fields appear empty until re-saved. - lib/proxy.ts: new restoreProxySettings() — the single hydration step (proxy_url / proxy_enabled / proxy_bypass), env-var precedence intact - index.ts: calls it instead of the three inline apply* calls - server-host.ts: calls it after initDb, before createApp - new proxy-restore.test.ts: pins DB → process-state hydration, the disabled flag, empty-DB defaults, and PROXY_URL env precedence Checked: server vitest (proxy + proxy-restore: 87 pass; one pre-existing compression perf failure on clean main, unrelated), desktop vitest, esbuild bundle of server-host.ts.
This commit is contained in:
@@ -11,6 +11,7 @@ import crypto from 'node:crypto';
|
||||
import type { Server } from 'node:http';
|
||||
import { createApp } from '../../server/src/app.js';
|
||||
import { initDb, getDb, getUnifiedApiKey } from '../../server/src/db/index.js';
|
||||
import { restoreProxySettings } from '../../server/src/lib/proxy.js';
|
||||
import { startHealthChecker } from '../../server/src/services/health.js';
|
||||
import { startCatalogSync } from '../../server/src/services/catalog-sync.js';
|
||||
import { userCount, createUser, createSession } from '../../server/src/services/auth.js';
|
||||
@@ -40,6 +41,11 @@ export async function startServer(opts: StartOptions): Promise<ServerHandle> {
|
||||
// 0.0.0.0, and a remote viewer must still enter the password.
|
||||
process.env.FREEAPI_DESKTOP = '1';
|
||||
initDb(opts.dbPath);
|
||||
// #949: the standalone server hydrates its proxy state in index.ts after
|
||||
// initDb; this embedder builds the app without index.ts, so without this
|
||||
// the URL saved in the settings table is ignored on every restart and the
|
||||
// outbound proxy fields appear empty until re-saved.
|
||||
restoreProxySettings();
|
||||
const app = createApp();
|
||||
const { server, port } = await listenWithScan(app, opts.host, opts.preferredPort);
|
||||
// Background timers need a Scheduler since the abstraction landed (4cbb571);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
|
||||
import { initDb, getDb, setSetting } from '../../db/index.js';
|
||||
import {
|
||||
restoreProxySettings,
|
||||
getProxyUrl,
|
||||
isProxyEnabled,
|
||||
getProxyBypassPlatforms,
|
||||
applyProxyUrl,
|
||||
applyProxyEnabled,
|
||||
applyProxyBypass,
|
||||
} from '../../lib/proxy.js';
|
||||
|
||||
// #949: the desktop embedder builds the app without server/src/index.ts, so
|
||||
// the proxy state it starts with is whatever the module defaults are — an
|
||||
// empty URL. The URL the user saved through PUT /api/settings/proxy sits in
|
||||
// the settings table, ignored until the next re-save. restoreProxySettings()
|
||||
// is the single hydration step both entry points now call after initDb; this
|
||||
// test pins that the DB value actually reaches the process state.
|
||||
|
||||
const PROXY_ENV_VARS = ['PROXY_URL', 'ALL_PROXY', 'HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY'];
|
||||
|
||||
function clearProxyEnv(): void {
|
||||
for (const name of PROXY_ENV_VARS) {
|
||||
delete process.env[name];
|
||||
delete process.env[name.toLowerCase()];
|
||||
}
|
||||
}
|
||||
|
||||
let closed = false;
|
||||
|
||||
beforeEach(() => {
|
||||
clearProxyEnv();
|
||||
// Reset to the module defaults so each case starts from "fresh process".
|
||||
applyProxyUrl('');
|
||||
applyProxyEnabled(true);
|
||||
applyProxyBypass('');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (!closed) {
|
||||
getDb().close();
|
||||
closed = true;
|
||||
}
|
||||
});
|
||||
|
||||
describe('restoreProxySettings (desktop embedder hydration, #949)', () => {
|
||||
it('loads a saved proxy URL, enabled flag and bypass list from the settings table', () => {
|
||||
process.env.ENCRYPTION_KEY = '0'.repeat(64);
|
||||
initDb(':memory:');
|
||||
setSetting('proxy_url', 'socks5h://127.0.0.1:9050');
|
||||
setSetting('proxy_enabled', '1');
|
||||
setSetting('proxy_bypass', 'groq,openrouter');
|
||||
|
||||
restoreProxySettings();
|
||||
|
||||
expect(getProxyUrl()).toBe('socks5h://127.0.0.1:9050');
|
||||
expect(isProxyEnabled()).toBe(true);
|
||||
expect(getProxyBypassPlatforms()).toEqual(['groq', 'openrouter']);
|
||||
});
|
||||
|
||||
it('respects a disabled proxy saved across a restart', () => {
|
||||
process.env.ENCRYPTION_KEY = '0'.repeat(64);
|
||||
initDb(':memory:');
|
||||
setSetting('proxy_url', 'http://127.0.0.1:3128');
|
||||
setSetting('proxy_enabled', '0');
|
||||
|
||||
restoreProxySettings();
|
||||
|
||||
expect(getProxyUrl()).toBe('http://127.0.0.1:3128');
|
||||
expect(isProxyEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the defaults when nothing was ever saved', () => {
|
||||
process.env.ENCRYPTION_KEY = '0'.repeat(64);
|
||||
initDb(':memory:');
|
||||
|
||||
restoreProxySettings();
|
||||
|
||||
expect(getProxyUrl()).toBe('');
|
||||
expect(isProxyEnabled()).toBe(true);
|
||||
expect(getProxyBypassPlatforms()).toEqual([]);
|
||||
});
|
||||
|
||||
it('lets the PROXY_URL env var outrank the saved value, as before', () => {
|
||||
process.env.ENCRYPTION_KEY = '0'.repeat(64);
|
||||
initDb(':memory:');
|
||||
setSetting('proxy_url', 'http://saved:3128');
|
||||
process.env.PROXY_URL = 'http://env:8080';
|
||||
|
||||
restoreProxySettings();
|
||||
|
||||
expect(getProxyUrl()).toBe('http://env:8080');
|
||||
});
|
||||
});
|
||||
+3
-5
@@ -1,8 +1,8 @@
|
||||
import './env.js';
|
||||
import { createApp } from './app.js';
|
||||
import { initDb, getDb, getSetting } from './db/index.js';
|
||||
import { initDb, getDb } from './db/index.js';
|
||||
import { startHealthChecker, checkAllKeys } from './services/health.js';
|
||||
import { applyProxyUrl, applyProxyEnabled, applyProxyBypass, flushProxyCache } from './lib/proxy.js';
|
||||
import { restoreProxySettings, flushProxyCache } from './lib/proxy.js';
|
||||
import { startWakeDetect } from './lib/wake-detect.js';
|
||||
import { startCatalogSync } from './services/catalog-sync.js';
|
||||
import { startCooldownProbe } from './services/cooldown-probe.js';
|
||||
@@ -64,9 +64,7 @@ async function main() {
|
||||
|
||||
// Load the persisted proxy settings from the DB (env var wins if set).
|
||||
// Must happen after initDb so the settings table is ready.
|
||||
applyProxyUrl(getSetting('proxy_url') ?? '');
|
||||
applyProxyEnabled(getSetting('proxy_enabled') !== '0'); // default: enabled
|
||||
applyProxyBypass(getSetting('proxy_bypass') ?? '');
|
||||
restoreProxySettings();
|
||||
|
||||
const app = createApp(config);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
import { getSetting } from '../db/index.js';
|
||||
import { assertProviderUrlAllowed } from './url-guard.js';
|
||||
|
||||
// #590 (per-key proxy): the SAME provider may be reached through different
|
||||
@@ -190,6 +191,22 @@ export function applyProxyUrl(dbValue: string): void {
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate the process-wide proxy state from the settings table.
|
||||
*
|
||||
* The standalone server does this in index.ts after initDb; the desktop
|
||||
* embedder (desktop/src/server-host.ts) builds the app without index.ts and
|
||||
* must call this itself — otherwise the URL saved by PUT /api/settings/proxy
|
||||
* sits in the DB but the process starts with an empty proxy and every
|
||||
* outbound request goes direct until the user re-saves the setting (#949).
|
||||
* Safe to call more than once; it is idempotent.
|
||||
*/
|
||||
export function restoreProxySettings(): void {
|
||||
applyProxyUrl(getSetting('proxy_url') ?? '');
|
||||
applyProxyEnabled(getSetting('proxy_enabled') !== '0'); // default: enabled
|
||||
applyProxyBypass(getSetting('proxy_bypass') ?? '');
|
||||
}
|
||||
|
||||
export function getProxyUrl(): string {
|
||||
return _proxyUrl;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user