fix: bootstrap gateway env proxy dispatcher

Co-authored-by: mjamiv <74088820+mjamiv@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-04-26 02:22:03 +01:00
parent c43ce254e1
commit d513dc7146
5 changed files with 141 additions and 0 deletions

View File

@@ -80,6 +80,9 @@ Docs: https://docs.openclaw.ai
- Gateway/Fly.io: seed Control UI allowed origins from the actual runtime
bind and port so CLI-driven non-loopback starts do not crash before config
exists. Fixes #71823.
- Gateway/proxy: bootstrap env proxy dispatching from direct Gateway startup
so provider and plugin network requests honor `HTTPS_PROXY`/`HTTP_PROXY`
before the first embedded agent attempt runs. (#71833) Thanks @mjamiv.
- Models/LM Studio: preserve `@iq*` quant suffixes in model refs and provider
matching so `/model lmstudio/...@iq3_xxs` keeps the exact LM Studio variant.
Fixes #71474. (#71486) Thanks @Bartok9, @XinwuC, and @Sanjays2402.

View File

@@ -0,0 +1,109 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { Agent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { clearAllBootstrapSnapshots } from "../agents/bootstrap-cache.js";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
import { clearSessionStoreCacheForTest } from "../config/sessions/store.js";
import { resetAgentRunContextForTest } from "../infra/agent-events.js";
import { PROXY_ENV_KEYS } from "../infra/net/proxy-env.js";
import { clearGatewaySubagentRuntime } from "../plugins/runtime/index.js";
import { captureEnv } from "../test-utils/env.js";
import { startGatewayServer } from "./server.js";
import { getFreeGatewayPort } from "./test-helpers.e2e.js";
const NETWORK_GATEWAY_ENV_KEYS = [
"HOME",
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_GATEWAY_TOKEN",
"OPENCLAW_SKIP_CHANNELS",
"OPENCLAW_SKIP_GMAIL_WATCHER",
"OPENCLAW_SKIP_CRON",
"OPENCLAW_SKIP_CANVAS_HOST",
"OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
"OPENCLAW_SKIP_PROVIDERS",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_TEST_MINIMAL_GATEWAY",
...PROXY_ENV_KEYS,
"NO_PROXY",
"no_proxy",
] as const;
function isEnvHttpProxyDispatcher(dispatcher: unknown): boolean {
return (
(dispatcher as { constructor?: { name?: string } } | undefined)?.constructor?.name ===
"EnvHttpProxyAgent"
);
}
describe("gateway network runtime", () => {
beforeEach(() => {
clearRuntimeConfigSnapshot();
clearConfigCache();
clearSessionStoreCacheForTest();
resetAgentRunContextForTest();
clearAllBootstrapSnapshots();
clearGatewaySubagentRuntime();
});
afterEach(() => {
clearRuntimeConfigSnapshot();
clearConfigCache();
clearSessionStoreCacheForTest();
resetAgentRunContextForTest();
clearAllBootstrapSnapshots();
clearGatewaySubagentRuntime();
});
it("bootstraps env proxy dispatching when the gateway starts directly", async () => {
const envSnapshot = captureEnv([...NETWORK_GATEWAY_ENV_KEYS]);
const originalDispatcher = getGlobalDispatcher();
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-proxy-home-"));
let server: Awaited<ReturnType<typeof startGatewayServer>> | undefined;
try {
setGlobalDispatcher(new Agent());
for (const key of NETWORK_GATEWAY_ENV_KEYS) {
delete process.env[key];
}
process.env.HTTPS_PROXY = "http://127.0.0.1:9";
process.env.HOME = tempHome;
process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw");
process.env.OPENCLAW_SKIP_CHANNELS = "1";
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
process.env.OPENCLAW_SKIP_CRON = "1";
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1";
process.env.OPENCLAW_SKIP_PROVIDERS = "1";
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "1";
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(tempHome, "empty-bundled-plugins");
await fs.mkdir(process.env.OPENCLAW_BUNDLED_PLUGINS_DIR, { recursive: true });
const token = `proxy-token-${process.pid}-${process.env.VITEST_POOL_ID ?? "0"}`;
process.env.OPENCLAW_GATEWAY_TOKEN = token;
const configPath = path.join(tempHome, ".openclaw", "openclaw.json");
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
`${JSON.stringify({ gateway: { auth: { mode: "token", token } } }, null, 2)}\n`,
);
process.env.OPENCLAW_CONFIG_PATH = configPath;
server = await startGatewayServer(await getFreeGatewayPort(), {
bind: "loopback",
auth: { mode: "token", token },
controlUiEnabled: false,
});
expect(isEnvHttpProxyDispatcher(getGlobalDispatcher())).toBe(true);
} finally {
await server?.close({ reason: "gateway proxy bootstrap test complete" });
setGlobalDispatcher(originalDispatcher);
await fs.rm(tempHome, { recursive: true, force: true });
envSnapshot.restore();
}
});
});

View File

@@ -0,0 +1,21 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const ensureGlobalUndiciEnvProxyDispatcherMock = vi.fn();
vi.mock("../infra/net/undici-global-dispatcher.js", () => ({
ensureGlobalUndiciEnvProxyDispatcher: ensureGlobalUndiciEnvProxyDispatcherMock,
}));
const { bootstrapGatewayNetworkRuntime } = await import("./server-network-runtime.js");
describe("bootstrapGatewayNetworkRuntime", () => {
beforeEach(() => {
ensureGlobalUndiciEnvProxyDispatcherMock.mockClear();
});
it("installs the env proxy dispatcher for gateway-owned network work", () => {
bootstrapGatewayNetworkRuntime();
expect(ensureGlobalUndiciEnvProxyDispatcherMock).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,5 @@
import { ensureGlobalUndiciEnvProxyDispatcher } from "../infra/net/undici-global-dispatcher.js";
export function bootstrapGatewayNetworkRuntime(): void {
ensureGlobalUndiciEnvProxyDispatcher();
}

View File

@@ -55,6 +55,7 @@ import { createGatewayServerLiveState, type GatewayServerLiveState } from "./ser
import { GATEWAY_EVENTS } from "./server-methods-list.js";
import { coreGatewayHandlers } from "./server-methods.js";
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
import { bootstrapGatewayNetworkRuntime } from "./server-network-runtime.js";
import { createGatewayNodeSessionRuntime } from "./server-node-session-runtime.js";
import { setFallbackGatewayContextResolver } from "./server-plugins.js";
import { startManagedGatewayConfigReloader } from "./server-reload-handlers.js";
@@ -244,6 +245,8 @@ export async function startGatewayServer(
port = 18789,
opts: GatewayServerOptions = {},
): Promise<GatewayServer> {
bootstrapGatewayNetworkRuntime();
const minimalTestGateway =
isVitestRuntimeEnv() && process.env.OPENCLAW_TEST_MINIMAL_GATEWAY === "1";