mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
G4: keep CLI-to-daemon loopback calls off HTTP_PROXY (#494)
The CLI appends 127.0.0.1, localhost and ::1 to the user's NO_PROXY (and no_proxy), keeping existing entries, before its first fetch. Verified on Bun 1.4.0: fetch honors NO_PROXY when it is set before the first request and ignores later changes, so it is set at the top of main().
This commit is contained in:
1 parent
fb4d2983e9
commit
616773158b
2 files changed
+91
No files matched your search
@@ -1605,7 +1605,21 @@ async function handlePairAgent(state: ServerState, args: string[]): Promise<void
|
||||
}
|
||||
|
||||
// ─── Main ──────────────────────────────────────────────────────
|
||||
/**
|
||||
* #494: the CLI talks to its daemon over loopback, but Bun's fetch sends those
|
||||
* calls through HTTP(S)_PROXY, so the daemon never looks healthy behind a
|
||||
* corporate proxy. Append the loopback names to the user's NO_PROXY (never
|
||||
* replace it). Bun reads NO_PROXY when the first fetch runs, so this must run
|
||||
* before any fetch. The daemon inherits the same value.
|
||||
*/
|
||||
export function withLoopbackNoProxy(env: Record<string, string | undefined>): string {
|
||||
const entries = (env.NO_PROXY ?? env.no_proxy ?? '').split(',').map(e => e.trim()).filter(Boolean);
|
||||
for (const host of ['127.0.0.1', 'localhost', '::1']) if (!entries.includes(host)) entries.push(host);
|
||||
return entries.join(',');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
process.env.NO_PROXY = process.env.no_proxy = withLoopbackNoProxy(process.env);
|
||||
const rawArgs = process.argv.slice(2);
|
||||
|
||||
// ─── Global flags (--proxy, --headed) ───────────────────────
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* G4 (#494): with HTTP_PROXY set, the CLI's loopback calls to its own daemon
|
||||
* went through the proxy, so the daemon never looked healthy. The CLI appends
|
||||
* loopback to NO_PROXY (keeping the user's entries) before its first fetch.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { withLoopbackNoProxy } from '../src/cli';
|
||||
|
||||
const CLI = path.resolve(import.meta.dir, '../src/cli.ts');
|
||||
|
||||
describe('withLoopbackNoProxy', () => {
|
||||
test('appends loopback names to an existing list without replacing it', () => {
|
||||
expect(withLoopbackNoProxy({ NO_PROXY: 'corp.internal, .example.com' })).toBe('corp.internal,.example.com,127.0.0.1,localhost,::1');
|
||||
expect(withLoopbackNoProxy({ no_proxy: 'corp.internal' })).toBe('corp.internal,127.0.0.1,localhost,::1');
|
||||
expect(withLoopbackNoProxy({})).toBe('127.0.0.1,localhost,::1');
|
||||
expect(withLoopbackNoProxy({ NO_PROXY: 'localhost,127.0.0.1,::1' })).toBe('localhost,127.0.0.1,::1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLI → daemon behind a proxy that fails every request', () => {
|
||||
let scratch: string;
|
||||
let proxy: ReturnType<typeof Bun.serve>;
|
||||
let proxied = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-no-proxy-'));
|
||||
proxied = 0;
|
||||
proxy = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => { proxied++; return new Response('proxy refuses', { status: 502 }); } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { proxy.stop(true); } catch {}
|
||||
const stateFile = path.join(scratch, 'state', 'browse.json');
|
||||
if (fs.existsSync(stateFile)) {
|
||||
try { process.kill(JSON.parse(fs.readFileSync(stateFile, 'utf-8')).pid, 'SIGKILL'); } catch {}
|
||||
}
|
||||
fs.rmSync(scratch, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test.skipIf(process.platform === 'win32')('status reaches the daemon directly and the daemon keeps the user NO_PROXY entries', async () => {
|
||||
const stateFile = path.join(scratch, 'state', 'browse.json');
|
||||
const stub = path.join(scratch, 'stub-server.ts');
|
||||
fs.writeFileSync(stub, `
|
||||
import * as fs from 'node:fs';
|
||||
const server = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch: () => Response.json({ status: 'healthy' }) });
|
||||
fs.writeFileSync(process.env.BROWSE_STATE_FILE!, JSON.stringify({
|
||||
pid: process.pid, port: server.port, token: 'no-proxy-test', mode: 'launched', noProxy: process.env.NO_PROXY,
|
||||
}));
|
||||
setTimeout(() => process.exit(0), 5000);
|
||||
`);
|
||||
const env: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value !== undefined && !/^(BROWSE_|GSTACK_|NO_PROXY$|no_proxy$|HTTPS?_PROXY$|https?_proxy$|ALL_PROXY$|all_proxy$)/.test(key)) env[key] = value;
|
||||
}
|
||||
Object.assign(env, {
|
||||
HOME: scratch,
|
||||
GSTACK_HOME: path.join(scratch, '.gstack'),
|
||||
BROWSE_STATE_FILE: stateFile,
|
||||
BROWSE_SERVER_SCRIPT: stub,
|
||||
BROWSE_PARENT_PID: '0',
|
||||
BROWSE_START_TIMEOUT: '8000',
|
||||
HTTP_PROXY: `http://127.0.0.1:${proxy.port}`,
|
||||
http_proxy: `http://127.0.0.1:${proxy.port}`,
|
||||
NO_PROXY: 'corp.internal',
|
||||
});
|
||||
const cli = Bun.spawn([process.execPath, CLI, 'status'], { cwd: scratch, env, stdout: 'pipe', stderr: 'pipe' });
|
||||
const timer = setTimeout(() => cli.kill('SIGKILL'), 30_000);
|
||||
const [code, stdout, stderr] = await Promise.all([cli.exited, new Response(cli.stdout).text(), new Response(cli.stderr).text()]);
|
||||
clearTimeout(timer);
|
||||
expect(code, stdout + stderr).toBe(0);
|
||||
expect(proxied).toBe(0);
|
||||
expect(JSON.parse(fs.readFileSync(stateFile, 'utf-8')).noProxy).toBe('corp.internal,127.0.0.1,localhost,::1');
|
||||
}, 45_000);
|
||||
});
|
||||
Reference in new issue
Block a user