From 22fc256c3c14dd5f37485796864beb1be0f23320 Mon Sep 17 00:00:00 2001 From: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Mon, 4 May 2026 17:22:42 +1000 Subject: [PATCH] fix: preserve nested proxy handles --- src/infra/net/proxy/active-proxy-state.ts | 24 +++++++++++++++------ src/infra/net/proxy/proxy-lifecycle.test.ts | 20 +++++++++++------ src/infra/net/proxy/proxy-lifecycle.ts | 23 +++++++++++++++----- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/infra/net/proxy/active-proxy-state.ts b/src/infra/net/proxy/active-proxy-state.ts index 23791f6a786..d884397a4b7 100644 --- a/src/infra/net/proxy/active-proxy-state.ts +++ b/src/infra/net/proxy/active-proxy-state.ts @@ -6,16 +6,23 @@ export type ActiveManagedProxyRegistration = { }; let activeProxyUrl: ActiveManagedProxyUrl | undefined; +let activeProxyRegistrationCount = 0; export function registerActiveManagedProxyUrl(proxyUrl: URL): ActiveManagedProxyRegistration { + const normalizedProxyUrl = new URL(proxyUrl.href); if (activeProxyUrl !== undefined) { - throw new Error( - "proxy: cannot activate a managed proxy while another proxy is active; " + - "stop the current proxy before changing proxy.proxyUrl.", - ); + if (activeProxyUrl.href !== normalizedProxyUrl.href) { + throw new Error( + "proxy: cannot activate a managed proxy while another proxy is active; " + + "stop the current proxy before changing proxy.proxyUrl.", + ); + } + activeProxyRegistrationCount += 1; + return { proxyUrl: activeProxyUrl, stopped: false }; } - activeProxyUrl = new URL(proxyUrl.href); + activeProxyUrl = normalizedProxyUrl; + activeProxyRegistrationCount = 1; return { proxyUrl: activeProxyUrl, stopped: false }; } @@ -26,7 +33,11 @@ export function stopActiveManagedProxyRegistration( return; } registration.stopped = true; - if (activeProxyUrl?.href === registration.proxyUrl.href) { + if (activeProxyUrl?.href !== registration.proxyUrl.href) { + return; + } + activeProxyRegistrationCount = Math.max(0, activeProxyRegistrationCount - 1); + if (activeProxyRegistrationCount === 0) { activeProxyUrl = undefined; } } @@ -37,4 +48,5 @@ export function getActiveManagedProxyUrl(): ActiveManagedProxyUrl | undefined { export function _resetActiveManagedProxyStateForTests(): void { activeProxyUrl = undefined; + activeProxyRegistrationCount = 0; } diff --git a/src/infra/net/proxy/proxy-lifecycle.test.ts b/src/infra/net/proxy/proxy-lifecycle.test.ts index b40ee26ecc0..bbb60417856 100644 --- a/src/infra/net/proxy/proxy-lifecycle.test.ts +++ b/src/infra/net/proxy/proxy-lifecycle.test.ts @@ -291,7 +291,7 @@ describe("startProxy", () => { expect((global as Record)["GLOBAL_AGENT"]).toBeUndefined(); }); - it("rejects overlapping handles with the same managed proxy URL", async () => { + it("keeps same-url overlapping handles active until the final stop", async () => { const patchedHttpRequest = vi.fn() as unknown as typeof http.request; const patchedHttpGet = vi.fn() as unknown as typeof http.get; const patchedHttpsRequest = vi.fn() as unknown as typeof https.request; @@ -311,13 +311,19 @@ describe("startProxy", () => { enabled: true, proxyUrl: "http://127.0.0.1:3128", }); + const secondHandle = await startProxy({ + enabled: true, + proxyUrl: "http://127.0.0.1:3128", + }); - await expect( - startProxy({ - enabled: true, - proxyUrl: "http://127.0.0.1:3128", - }), - ).rejects.toThrow("cannot activate a managed proxy"); + expect(mockForceResetGlobalDispatcher).toHaveBeenCalledOnce(); + expect(mockBootstrapGlobalAgent).toHaveBeenCalledOnce(); + expect(http.request).toBe(patchedHttpRequest); + expect(https.request).toBe(patchedHttpsRequest); + expect(process.env["HTTP_PROXY"]).toBe("http://127.0.0.1:3128"); + expect(process.env["OPENCLAW_PROXY_ACTIVE"]).toBe("1"); + + await stopProxy(secondHandle); expect(http.request).toBe(patchedHttpRequest); expect(https.request).toBe(patchedHttpsRequest); diff --git a/src/infra/net/proxy/proxy-lifecycle.ts b/src/infra/net/proxy/proxy-lifecycle.ts index a05790cfe5b..98f57d1fcec 100644 --- a/src/infra/net/proxy/proxy-lifecycle.ts +++ b/src/infra/net/proxy/proxy-lifecycle.ts @@ -343,6 +343,9 @@ function stopActiveProxyRegistration(registration: ActiveManagedProxyRegistratio return; } stopActiveManagedProxyRegistration(registration); + if (getActiveManagedProxyUrl()) { + return; + } const restoreSnapshot = baseProxyEnvSnapshot ?? captureProxyEnv(); baseProxyEnvSnapshot = null; @@ -390,11 +393,21 @@ export async function startProxy(config: ProxyConfig | undefined): Promise { + stopActiveProxyRegistration(registration); + }, + kill: () => { + stopActiveProxyRegistration(registration); + }, + }; + return handle; } baseProxyEnvSnapshot ??= captureProxyEnv(); const lifecycleBaseEnvSnapshot = baseProxyEnvSnapshot;