From aaebbf22b99717593b328179836636d44ea6e715 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 7 Jul 2026 10:59:50 +0000 Subject: [PATCH] fix(cli): honor HTTP_PROXY/HTTPS_PROXY for Node.js native fetch Node native fetch ignores proxy env vars by default. In corporate proxy environments (e.g. Zscaler) this causes connect timeouts for all backend calls because traffic bypasses the configured proxy. - Add @githits/core-internal/shared/proxy-fetch with configureProxyAwareFetch() using undici EnvHttpProxyAgent to read HTTP_PROXY/HTTPS_PROXY/NO_PROXY. - Call configureProxyAwareFetch() at the top of src/cli.ts before any network work, and inside createContainer() so MCP and programmatic usage are covered. - Add unit tests for hasProxyEnv and configureProxyAwareFetch. - Add undici ^8.5.0 as a dependency. --- bun.lock | 3 ++ package.json | 1 + packages/core-internal/src/index.ts | 4 ++ .../src/shared/proxy-fetch.test.ts | 43 +++++++++++++++++ .../core-internal/src/shared/proxy-fetch.ts | 47 +++++++++++++++++++ src/cli.ts | 6 +++ src/container.ts | 5 ++ 7 files changed, 109 insertions(+) create mode 100644 packages/core-internal/src/shared/proxy-fetch.test.ts create mode 100644 packages/core-internal/src/shared/proxy-fetch.ts diff --git a/bun.lock b/bun.lock index a2eddbbb..9dac6c2c 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,7 @@ "open": "^11.0.0", "semver": "7.8.5", "smol-toml": "^1.6.1", + "undici": "^8.5.0", "yaml": "^2.9.0", "zod": "^4.4.3", }, @@ -558,6 +559,8 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "undici": ["undici@8.6.0", "", {}, "sha512-l2FlC6I510GawyEd1qgcE/okihKrzy+BRTEBlu6T0fdbM9m5yxtIH5Oa3ysRsH0zC4EhmWUEaSDsy2QngBeRlw=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], diff --git a/package.json b/package.json index ddf4db7c..b3485b8f 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "open": "^11.0.0", "semver": "7.8.5", "smol-toml": "^1.6.1", + "undici": "^8.5.0", "yaml": "^2.9.0", "zod": "^4.4.3" }, diff --git a/packages/core-internal/src/index.ts b/packages/core-internal/src/index.ts index 9f078548..f49566ff 100644 --- a/packages/core-internal/src/index.ts +++ b/packages/core-internal/src/index.ts @@ -12,5 +12,9 @@ export * from "./shared/debug-log.js"; export * from "./shared/fetch-timeout.js"; export * from "./shared/pkgseer-graphql.js"; export * from "./shared/pkgseer-registry.js"; +export { + configureProxyAwareFetch, + hasProxyEnv, +} from "./shared/proxy-fetch.js"; export * from "./shared/request-headers.js"; export * from "./shared/telemetry.js"; diff --git a/packages/core-internal/src/shared/proxy-fetch.test.ts b/packages/core-internal/src/shared/proxy-fetch.test.ts new file mode 100644 index 00000000..624e4b4e --- /dev/null +++ b/packages/core-internal/src/shared/proxy-fetch.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "bun:test"; +import { configureProxyAwareFetch, hasProxyEnv } from "./proxy-fetch.js"; + +describe("hasProxyEnv", () => { + it("returns false when no proxy variables are set", () => { + const env = {}; + expect(hasProxyEnv(env)).toBe(false); + }); + + it("returns true when HTTPS_PROXY is set", () => { + const env = { HTTPS_PROXY: "http://proxy.example.com:8080" }; + expect(hasProxyEnv(env)).toBe(true); + }); + + it("returns true when https_proxy is set", () => { + const env = { https_proxy: "http://proxy.example.com:8080" }; + expect(hasProxyEnv(env)).toBe(true); + }); + + it("returns true when HTTP_PROXY is set", () => { + const env = { HTTP_PROXY: "http://proxy.example.com:8080" }; + expect(hasProxyEnv(env)).toBe(true); + }); + + it("returns true when http_proxy is set", () => { + const env = { http_proxy: "http://proxy.example.com:8080" }; + expect(hasProxyEnv(env)).toBe(true); + }); +}); + +describe("configureProxyAwareFetch", () => { + it("does not throw when proxy variables are set", () => { + expect(() => + configureProxyAwareFetch({ + HTTPS_PROXY: "http://proxy.example.com:8080", + }), + ).not.toThrow(); + }); + + it("does not throw when no proxy variables are set", () => { + expect(() => configureProxyAwareFetch({})).not.toThrow(); + }); +}); diff --git a/packages/core-internal/src/shared/proxy-fetch.ts b/packages/core-internal/src/shared/proxy-fetch.ts new file mode 100644 index 00000000..2d64eb10 --- /dev/null +++ b/packages/core-internal/src/shared/proxy-fetch.ts @@ -0,0 +1,47 @@ +/** + * Proxy-aware fetch setup. + * + * Node.js native `fetch` ignores `HTTP_PROXY`/`HTTPS_PROXY` by default. This + * module detects the standard proxy environment variables and, when present, + * installs an `undici` dispatcher so that all subsequent native fetch calls + * route through the proxy. + * + * Supports `NO_PROXY` for selective opt-out, matching common corporate proxy + * conventions. + */ +import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; + +let proxyFetchConfigured = false; + +/** + * Returns true when at least one of the standard proxy environment variables + * is set. Checks lower- and upper-case variants, matching curl/git convention. + */ +export function hasProxyEnv(env: NodeJS.ProcessEnv = process.env): boolean { + return Boolean( + env.HTTP_PROXY || env.http_proxy || env.HTTPS_PROXY || env.https_proxy, + ); +} + +/** + * Configures Node.js native fetch to honor `HTTP_PROXY`/`HTTPS_PROXY` via + * undici's `EnvHttpProxyAgent`. Safe to call multiple times; subsequent calls + * are no-ops. + * + * This should run as early as possible in the process lifetime, before any + * network request is issued. + */ +export function configureProxyAwareFetch( + env: NodeJS.ProcessEnv = process.env, +): void { + if (proxyFetchConfigured) { + return; + } + if (!hasProxyEnv(env)) { + return; + } + + const agent = new EnvHttpProxyAgent(); + setGlobalDispatcher(agent); + proxyFetchConfigured = true; +} diff --git a/src/cli.ts b/src/cli.ts index 3f2ac252..9a1ddfca 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,11 +1,17 @@ #!/usr/bin/env node import { + configureProxyAwareFetch, endTelemetrySpan, flushTelemetry, isTelemetryEnabled, startTelemetrySpan, withTelemetrySpan, } from "@githits/core-internal"; + +// Honor HTTP_PROXY/HTTPS_PROXY for all native fetch calls. Must run before +// any network request so that corporate proxy environments work out of the box. +configureProxyAwareFetch(); + import { colorizeBrand, shouldUseColors } from "@githits/mcp/internal"; import { Command } from "commander"; import { version } from "../package.json"; diff --git a/src/container.ts b/src/container.ts index 3d426fdf..f81c0346 100644 --- a/src/container.ts +++ b/src/container.ts @@ -2,6 +2,7 @@ import { type AgentInfo, type CodeNavigationService, CodeNavigationServiceImpl, + configureProxyAwareFetch, createClientHeaderBuilder, createStaticTokenProvider, endTelemetrySpan, @@ -266,6 +267,10 @@ export interface CreateContainerOptions { export async function createContainer( options: CreateContainerOptions = {}, ): Promise { + // Ensure proxy env vars are honored for all service fetch calls. Idempotent; + // has no effect when HTTP_PROXY/HTTPS_PROXY are not set. + configureProxyAwareFetch(); + return withTelemetrySpan("container.create", async () => { const resolveStoredToken = options.resolveStoredToken ?? true; const mcpUrl = getMcpUrl();