Compare commits
3 Commits
main
...
fix/gaxios
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df41a200a5 | ||
|
|
56113720eb | ||
|
|
93131d9d9b |
@ -92,6 +92,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
|
||||
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
|
||||
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
|
||||
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
|
||||
@ -41,6 +41,9 @@ if (
|
||||
) {
|
||||
// Imported as a dependency — skip all entry-point side effects.
|
||||
} else {
|
||||
const { installGaxiosFetchCompat } = await import("./infra/gaxios-fetch-compat.js");
|
||||
|
||||
installGaxiosFetchCompat();
|
||||
process.title = "openclaw";
|
||||
ensureOpenClawExecMarkerOnProcess();
|
||||
installProcessWarningFilter();
|
||||
|
||||
@ -34,6 +34,20 @@ describe("legacy root entry", () => {
|
||||
expect(runtimeMocks.runCli).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps library imports free of global window shims", async () => {
|
||||
const originalWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
|
||||
Reflect.deleteProperty(globalThis as object, "window");
|
||||
|
||||
try {
|
||||
await import("./index.js");
|
||||
expect("window" in globalThis).toBe(false);
|
||||
} finally {
|
||||
if (originalWindowDescriptor) {
|
||||
Object.defineProperty(globalThis, "window", originalWindowDescriptor);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("delegates legacy direct-entry execution to run-main", async () => {
|
||||
const mod = await import("./index.js");
|
||||
const argv = ["node", "dist/index.js", "status"];
|
||||
|
||||
@ -32,7 +32,12 @@ export const waitForever = library.waitForever;
|
||||
|
||||
// Legacy direct file entrypoint only. Package root exports now live in library.ts.
|
||||
export async function runLegacyCliEntry(argv: string[] = process.argv): Promise<void> {
|
||||
const { runCli } = await import("./cli/run-main.js");
|
||||
const [{ installGaxiosFetchCompat }, { runCli }] = await Promise.all([
|
||||
import("./infra/gaxios-fetch-compat.js"),
|
||||
import("./cli/run-main.js"),
|
||||
]);
|
||||
|
||||
installGaxiosFetchCompat();
|
||||
await runCli(argv);
|
||||
}
|
||||
|
||||
|
||||
60
src/infra/gaxios-fetch-compat.test.ts
Normal file
60
src/infra/gaxios-fetch-compat.test.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { ProxyAgent } from "undici";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("gaxios fetch compat", () => {
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("uses native fetch without defining window or importing node-fetch", async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>(async () => {
|
||||
return new Response("ok", {
|
||||
headers: { "content-type": "text/plain" },
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
vi.doMock("node-fetch", () => {
|
||||
throw new Error("node-fetch should not load");
|
||||
});
|
||||
|
||||
const { installGaxiosFetchCompat } = await import("./gaxios-fetch-compat.js");
|
||||
const { Gaxios } = await import("gaxios");
|
||||
|
||||
installGaxiosFetchCompat();
|
||||
|
||||
const res = await new Gaxios().request({
|
||||
responseType: "text",
|
||||
url: "https://example.com",
|
||||
});
|
||||
|
||||
expect(res.data).toBe("ok");
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
expect("window" in globalThis).toBe(false);
|
||||
});
|
||||
|
||||
it("translates proxy agents into undici dispatchers for native fetch", async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>(async () => {
|
||||
return new Response("ok", {
|
||||
headers: { "content-type": "text/plain" },
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
const { createGaxiosCompatFetch } = await import("./gaxios-fetch-compat.js");
|
||||
|
||||
const compatFetch = createGaxiosCompatFetch(fetchMock);
|
||||
await compatFetch("https://example.com", {
|
||||
agent: new HttpsProxyAgent("http://proxy.example:8080"),
|
||||
} as RequestInit);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const [, init] = fetchMock.mock.calls[0] ?? [];
|
||||
|
||||
expect(init).not.toHaveProperty("agent");
|
||||
expect((init as { dispatcher?: unknown })?.dispatcher).toBeInstanceOf(ProxyAgent);
|
||||
});
|
||||
});
|
||||
212
src/infra/gaxios-fetch-compat.ts
Normal file
212
src/infra/gaxios-fetch-compat.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import type { ConnectionOptions } from "node:tls";
|
||||
import { Gaxios } from "gaxios";
|
||||
import type { Dispatcher } from "undici";
|
||||
import { Agent as UndiciAgent, ProxyAgent } from "undici";
|
||||
|
||||
type ProxyRule = RegExp | URL | string;
|
||||
type TlsCert = ConnectionOptions["cert"];
|
||||
type TlsKey = ConnectionOptions["key"];
|
||||
|
||||
type GaxiosFetchRequestInit = RequestInit & {
|
||||
agent?: unknown;
|
||||
cert?: TlsCert;
|
||||
dispatcher?: Dispatcher;
|
||||
fetchImplementation?: typeof fetch;
|
||||
key?: TlsKey;
|
||||
noProxy?: ProxyRule[];
|
||||
proxy?: string | URL;
|
||||
};
|
||||
|
||||
type ProxyAgentLike = {
|
||||
connectOpts?: { cert?: TlsCert; key?: TlsKey };
|
||||
proxy: URL;
|
||||
};
|
||||
|
||||
type TlsAgentLike = {
|
||||
options?: { cert?: TlsCert; key?: TlsKey };
|
||||
};
|
||||
|
||||
type GaxiosPrototype = {
|
||||
_defaultAdapter: (this: Gaxios, config: GaxiosFetchRequestInit) => Promise<unknown>;
|
||||
};
|
||||
|
||||
let installState: "not-installed" | "installed" = "not-installed";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function hasDispatcher(value: unknown): value is Dispatcher {
|
||||
return isRecord(value) && typeof value.dispatch === "function";
|
||||
}
|
||||
|
||||
function hasProxyAgentShape(value: unknown): value is ProxyAgentLike {
|
||||
return isRecord(value) && value.proxy instanceof URL;
|
||||
}
|
||||
|
||||
function hasTlsAgentShape(value: unknown): value is TlsAgentLike {
|
||||
return isRecord(value) && isRecord(value.options);
|
||||
}
|
||||
|
||||
function resolveTlsOptions(
|
||||
init: GaxiosFetchRequestInit,
|
||||
url: URL,
|
||||
): { cert?: TlsCert; key?: TlsKey } {
|
||||
const explicit = {
|
||||
cert: init.cert,
|
||||
key: init.key,
|
||||
};
|
||||
if (explicit.cert !== undefined || explicit.key !== undefined) {
|
||||
return explicit;
|
||||
}
|
||||
|
||||
const agent = typeof init.agent === "function" ? init.agent(url) : init.agent;
|
||||
if (hasProxyAgentShape(agent)) {
|
||||
return {
|
||||
cert: agent.connectOpts?.cert,
|
||||
key: agent.connectOpts?.key,
|
||||
};
|
||||
}
|
||||
if (hasTlsAgentShape(agent)) {
|
||||
return {
|
||||
cert: agent.options?.cert,
|
||||
key: agent.options?.key,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function urlMayUseProxy(url: URL, noProxy: ProxyRule[] = []): boolean {
|
||||
const rules = [...noProxy];
|
||||
const envRules = (process.env.NO_PROXY ?? process.env.no_proxy)?.split(",") ?? [];
|
||||
for (const rule of envRules) {
|
||||
const trimmed = rule.trim();
|
||||
if (trimmed.length > 0) {
|
||||
rules.push(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
for (const rule of rules) {
|
||||
if (rule instanceof RegExp) {
|
||||
if (rule.test(url.toString())) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (rule instanceof URL) {
|
||||
if (rule.origin === url.origin) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (rule.startsWith("*.") || rule.startsWith(".")) {
|
||||
const cleanedRule = rule.replace(/^\*\./, ".");
|
||||
if (url.hostname.endsWith(cleanedRule)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (rule === url.origin || rule === url.hostname || rule === url.href) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveProxyUri(init: GaxiosFetchRequestInit, url: URL): string | undefined {
|
||||
if (init.proxy) {
|
||||
const proxyUri = String(init.proxy);
|
||||
return urlMayUseProxy(url, init.noProxy) ? proxyUri : undefined;
|
||||
}
|
||||
|
||||
const envProxy =
|
||||
process.env.HTTPS_PROXY ??
|
||||
process.env.https_proxy ??
|
||||
process.env.HTTP_PROXY ??
|
||||
process.env.http_proxy;
|
||||
if (!envProxy) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return urlMayUseProxy(url, init.noProxy) ? envProxy : undefined;
|
||||
}
|
||||
|
||||
function buildDispatcher(init: GaxiosFetchRequestInit, url: URL): Dispatcher | undefined {
|
||||
if (init.dispatcher) {
|
||||
return init.dispatcher;
|
||||
}
|
||||
|
||||
const agent = typeof init.agent === "function" ? init.agent(url) : init.agent;
|
||||
if (hasDispatcher(agent)) {
|
||||
return agent;
|
||||
}
|
||||
|
||||
const { cert, key } = resolveTlsOptions(init, url);
|
||||
const proxyUri =
|
||||
resolveProxyUri(init, url) ?? (hasProxyAgentShape(agent) ? String(agent.proxy) : undefined);
|
||||
if (proxyUri) {
|
||||
return new ProxyAgent({
|
||||
requestTls: cert !== undefined || key !== undefined ? { cert, key } : undefined,
|
||||
uri: proxyUri,
|
||||
});
|
||||
}
|
||||
|
||||
if (cert !== undefined || key !== undefined) {
|
||||
return new UndiciAgent({
|
||||
connect: { cert, key },
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function createGaxiosCompatFetch(baseFetch: typeof fetch = globalThis.fetch): typeof fetch {
|
||||
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const gaxiosInit = (init ?? {}) as GaxiosFetchRequestInit;
|
||||
const requestUrl =
|
||||
input instanceof Request
|
||||
? new URL(input.url)
|
||||
: new URL(typeof input === "string" ? input : input.toString());
|
||||
const dispatcher = buildDispatcher(gaxiosInit, requestUrl);
|
||||
|
||||
const nextInit: RequestInit = { ...gaxiosInit };
|
||||
delete (nextInit as GaxiosFetchRequestInit).agent;
|
||||
delete (nextInit as GaxiosFetchRequestInit).cert;
|
||||
delete (nextInit as GaxiosFetchRequestInit).fetchImplementation;
|
||||
delete (nextInit as GaxiosFetchRequestInit).key;
|
||||
delete (nextInit as GaxiosFetchRequestInit).noProxy;
|
||||
delete (nextInit as GaxiosFetchRequestInit).proxy;
|
||||
|
||||
if (dispatcher) {
|
||||
(nextInit as RequestInit & { dispatcher: Dispatcher }).dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
return baseFetch(input, nextInit);
|
||||
};
|
||||
}
|
||||
|
||||
export function installGaxiosFetchCompat(): void {
|
||||
if (installState === "installed" || typeof globalThis.fetch !== "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
const prototype = Gaxios.prototype as unknown as GaxiosPrototype;
|
||||
const originalDefaultAdapter = prototype._defaultAdapter;
|
||||
const compatFetch = createGaxiosCompatFetch();
|
||||
|
||||
prototype._defaultAdapter = function patchedDefaultAdapter(
|
||||
this: Gaxios,
|
||||
config: GaxiosFetchRequestInit,
|
||||
): Promise<unknown> {
|
||||
if (config.fetchImplementation) {
|
||||
return originalDefaultAdapter.call(this, config);
|
||||
}
|
||||
return originalDefaultAdapter.call(this, {
|
||||
...config,
|
||||
fetchImplementation: compatFetch,
|
||||
});
|
||||
};
|
||||
|
||||
installState = "installed";
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user