Merge pull request #9453 from truffle-dev/fix/vscode-forward-http-proxy-to-cli

fix(vscode): forward VS Code http.proxy settings to spawned CLI process
This commit is contained in:
Marian Alexandru Alecu
2026-05-06 17:19:23 +03:00
committed by GitHub
2 changed files with 210 additions and 0 deletions
@@ -104,6 +104,12 @@ export class ServerManager {
...(extraCaCerts && { NODE_EXTRA_CA_CERTS: extraCaCerts }),
...(!proxyStrictSSL && { NODE_TLS_REJECT_UNAUTHORIZED: "0" }),
...process.env,
// VS Code's http.proxy / http.noProxy settings are not reflected in
// process.env, so spawned children bypass the user's configured proxy
// and fail behind corporate firewalls. Forward them as the standard
// HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars that Bun's fetch and
// most HTTP clients already respect.
...buildProxyEnv(),
// Force mimalloc (the allocator Bun ships with) to return freed pages
// to the OS immediately instead of retaining them in its arenas.
// Without this, Bun.spawn's piped stdio accumulates ~2 MB of native
@@ -257,6 +263,72 @@ function stripAnsi(str: string): string {
return str.replace(/\x1b\[[0-9;]*m/g, "")
}
/**
* Translate VS Code's `http.proxy` / `http.noProxy` / `http.proxySupport`
* settings into the standard proxy env vars, so the spawned CLI honors the
* user's proxy configuration. Returns an empty object when no override is
* needed, so callers can spread unconditionally.
*
* `http.proxySupport: "off"` is VS Code's opt-in way to disable proxy support
* entirely; when set, we explicitly clear the env vars so ambient shell
* HTTP_PROXY/http_proxy doesn't leak into the spawned child.
*/
export function buildProxyEnv(): Record<string, string> {
const httpConfig = vscode.workspace.getConfiguration("http")
const proxyInfo = httpConfig.inspect<string>("proxy")
const noProxyInfo = httpConfig.inspect<string[]>("noProxy")
const proxySupport = httpConfig.get<string>("proxySupport")
if (proxySupport === "off") {
return { HTTP_PROXY: "", HTTPS_PROXY: "", NO_PROXY: "", http_proxy: "", https_proxy: "", no_proxy: "" }
}
const proxy = httpConfig.get<string>("proxy")
const noProxy = httpConfig.get<string[]>("noProxy")
const proxySet =
proxyInfo !== undefined &&
[
proxyInfo.globalValue,
proxyInfo.workspaceValue,
proxyInfo.workspaceFolderValue,
proxyInfo.globalLanguageValue,
proxyInfo.workspaceLanguageValue,
proxyInfo.workspaceFolderLanguageValue,
].some((value) => value !== undefined)
const noProxySet =
noProxyInfo !== undefined &&
[
noProxyInfo.globalValue,
noProxyInfo.workspaceValue,
noProxyInfo.workspaceFolderValue,
noProxyInfo.globalLanguageValue,
noProxyInfo.workspaceLanguageValue,
noProxyInfo.workspaceFolderLanguageValue,
].some((value) => value !== undefined)
const env: Record<string, string> = {}
if (proxy && proxy.trim() !== "") {
env.HTTP_PROXY = proxy
env.HTTPS_PROXY = proxy
env.http_proxy = proxy
env.https_proxy = proxy
}
if (proxySet && proxy !== undefined && proxy.trim() === "") {
env.HTTP_PROXY = ""
env.HTTPS_PROXY = ""
env.http_proxy = ""
env.https_proxy = ""
}
if (Array.isArray(noProxy) && noProxy.length > 0) {
env.NO_PROXY = noProxy.join(",")
env.no_proxy = noProxy.join(",")
}
if (noProxySet && Array.isArray(noProxy) && noProxy.length === 0) {
env.NO_PROXY = ""
env.no_proxy = ""
}
return env
}
export function toErrorMessage(
error: string,
stderrLines: string[],
@@ -0,0 +1,138 @@
import { describe, it, expect, afterEach } from "bun:test"
import * as vscode from "vscode"
import { buildProxyEnv } from "../../src/services/cli-backend/server-manager"
type Info = { globalValue?: unknown; workspaceValue?: unknown; workspaceFolderValue?: unknown }
type WorkspaceStub = {
getConfiguration: (section?: string) => { get: (key: string) => unknown; inspect: (key: string) => Info }
}
const workspace = vscode.workspace as unknown as WorkspaceStub
const originalGetConfiguration = workspace.getConfiguration
function stubHttpConfig(values: { proxy?: unknown; noProxy?: unknown; proxySupport?: unknown }): void {
workspace.getConfiguration = (section?: string) => {
if (section === "http") {
return {
get: (key: string) => {
if (key === "proxy") return values.proxy
if (key === "noProxy") return values.noProxy
if (key === "proxySupport") return values.proxySupport
return undefined
},
inspect: (key: string) => {
if (key === "proxy" && values.proxy !== undefined) return { workspaceValue: values.proxy }
if (key === "noProxy" && values.noProxy !== undefined) return { workspaceValue: values.noProxy }
if (key === "proxySupport" && values.proxySupport !== undefined)
return { workspaceValue: values.proxySupport }
return {}
},
}
}
return { get: () => undefined, inspect: () => ({}) }
}
}
afterEach(() => {
workspace.getConfiguration = originalGetConfiguration
})
describe("buildProxyEnv", () => {
it("returns an empty object when neither proxy nor noProxy is configured", () => {
stubHttpConfig({ proxy: undefined, noProxy: undefined })
expect(buildProxyEnv()).toEqual({})
})
it("forwards http.proxy as HTTP_PROXY and HTTPS_PROXY", () => {
stubHttpConfig({ proxy: "http://proxy.corp.example:8080" })
expect(buildProxyEnv()).toEqual({
HTTP_PROXY: "http://proxy.corp.example:8080",
HTTPS_PROXY: "http://proxy.corp.example:8080",
http_proxy: "http://proxy.corp.example:8080",
https_proxy: "http://proxy.corp.example:8080",
})
})
it("joins http.noProxy into a comma-separated NO_PROXY value", () => {
stubHttpConfig({ noProxy: ["localhost", "127.0.0.1", "*.internal"] })
expect(buildProxyEnv()).toEqual({
NO_PROXY: "localhost,127.0.0.1,*.internal",
no_proxy: "localhost,127.0.0.1,*.internal",
})
})
it("forwards both proxy and noProxy when both are configured", () => {
stubHttpConfig({
proxy: "http://proxy.corp.example:8080",
noProxy: ["localhost", "*.internal"],
})
expect(buildProxyEnv()).toEqual({
HTTP_PROXY: "http://proxy.corp.example:8080",
HTTPS_PROXY: "http://proxy.corp.example:8080",
NO_PROXY: "localhost,*.internal",
http_proxy: "http://proxy.corp.example:8080",
https_proxy: "http://proxy.corp.example:8080",
no_proxy: "localhost,*.internal",
})
})
it("clears env vars when http.proxy is only whitespace", () => {
stubHttpConfig({ proxy: " " })
expect(buildProxyEnv()).toEqual({
HTTP_PROXY: "",
HTTPS_PROXY: "",
http_proxy: "",
https_proxy: "",
})
})
it("clears env var when http.noProxy is an empty array", () => {
stubHttpConfig({ noProxy: [] })
expect(buildProxyEnv()).toEqual({
NO_PROXY: "",
no_proxy: "",
})
})
it("ignores a non-array http.noProxy value", () => {
stubHttpConfig({ noProxy: "localhost" })
expect(buildProxyEnv()).toEqual({})
})
it("explicitly clears env vars when http.proxySupport is off", () => {
stubHttpConfig({ proxySupport: "off" })
expect(buildProxyEnv()).toEqual({
HTTP_PROXY: "",
HTTPS_PROXY: "",
NO_PROXY: "",
http_proxy: "",
https_proxy: "",
no_proxy: "",
})
})
it("http.proxySupport=off wins over a configured http.proxy/http.noProxy", () => {
stubHttpConfig({
proxy: "http://proxy.corp.example:8080",
noProxy: ["localhost"],
proxySupport: "off",
})
expect(buildProxyEnv()).toEqual({
HTTP_PROXY: "",
HTTPS_PROXY: "",
NO_PROXY: "",
http_proxy: "",
https_proxy: "",
no_proxy: "",
})
})
})