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();