From f9c639e1b49d268f5777358936f78684ff611cd9 Mon Sep 17 00:00:00 2001 From: Neda Kaighobadi Date: Tue, 26 May 2026 10:58:27 +0300 Subject: [PATCH 1/6] implement proxy --- src/api/index.ts | 2 ++ src/common/proxy.ts | 66 +++++++++++++++++++++++++++++++++++++++++ src/common/telemetry.ts | 6 +++- src/extension.ts | 10 +++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/common/proxy.ts diff --git a/src/api/index.ts b/src/api/index.ts index 0229b01..528761f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -23,8 +23,10 @@ import { import { Config } from '../common/config' import { Tools } from '../codacy/Tools' import { detectEditor } from '../auth' +import { configureAxiosProxy } from '../common/proxy' export const initializeApi = () => { + configureAxiosProxy() const ide = detectEditor() // set up OpenAPI client diff --git a/src/common/proxy.ts b/src/common/proxy.ts new file mode 100644 index 0000000..023780d --- /dev/null +++ b/src/common/proxy.ts @@ -0,0 +1,66 @@ +import * as vscode from 'vscode' +import { HttpsProxyAgent } from 'https-proxy-agent' +import { HttpProxyAgent } from 'http-proxy-agent' +import axios from 'axios' +import type { HTTPClient, HTTPClientRequest, HTTPResponse } from '@segment/analytics-node' + +function resolveProxyUrl(): string | undefined { + // VS Code setting takes precedence; an empty string means "no proxy" + const vscodeProxy = vscode.workspace.getConfiguration('http').get('proxy') + if (vscodeProxy !== undefined) { + return vscodeProxy || undefined + } + + return ( + process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || undefined + ) +} + +function resolveStrictSSL(): boolean { + return vscode.workspace.getConfiguration('http').get('proxyStrictSSL', true) +} + +function resolveProxyAuthorization(): string | undefined { + return vscode.workspace.getConfiguration('http').get('proxyAuthorization') ?? undefined +} + +/** + * Applies proxy settings from VS Code config and environment variables to the global axios instance + */ +export function configureAxiosProxy(): void { + const proxyUrl = resolveProxyUrl() + + if (proxyUrl) { + const strictSSL = resolveStrictSSL() + const authHeader = resolveProxyAuthorization() + const extraHeaders = authHeader ? { 'Proxy-Authorization': authHeader } : undefined + + axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, { + rejectUnauthorized: strictSSL, + ...(extraHeaders ? { headers: extraHeaders } : {}), + }) + axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, { + rejectUnauthorized: strictSSL, + }) + // Disable axios's built-in proxy parsing so the agents take full control + axios.defaults.proxy = false + } else { + axios.defaults.httpsAgent = undefined + axios.defaults.httpAgent = undefined + axios.defaults.proxy = undefined + } +} + +/** + * An HTTPClient implementation for @segment/analytics-node that routes + * requests through the proxy configured in axios defaults (if any). + */ +export class ProxiedSegmentHTTPClient implements HTTPClient { + async makeRequest(options: HTTPClientRequest): Promise { + const response = await axios.post(options.url, options.data, { + headers: options.headers, + timeout: options.httpRequestTimeout, + }) + return { status: response.status, statusText: response.statusText } + } +} diff --git a/src/common/telemetry.ts b/src/common/telemetry.ts index c901ce6..4125f0f 100644 --- a/src/common/telemetry.ts +++ b/src/common/telemetry.ts @@ -4,6 +4,7 @@ import { EventProperties } from '@segment/analytics-core' import { Organization, User } from '../api/client' import { SEGMENT_WRITE_KEY } from '../env-secrets' import { v4 as uuidv4 } from 'uuid' +import { ProxiedSegmentHTTPClient } from './proxy' class TelemetryClient { private analytics: Analytics | undefined @@ -13,7 +14,10 @@ class TelemetryClient { constructor() { if (SEGMENT_WRITE_KEY) { - this.analytics = new Analytics({ writeKey: SEGMENT_WRITE_KEY }) + this.analytics = new Analytics({ + writeKey: SEGMENT_WRITE_KEY, + httpClient: new ProxiedSegmentHTTPClient(), + }) } } diff --git a/src/extension.ts b/src/extension.ts index 7edeb2a..d72812e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import * as os from 'os' import { CommandType, wrapExtensionCommand } from './common/utils' import Logger from './common/logger' import { initializeApi } from './api' +import { configureAxiosProxy } from './common/proxy' import { GitProvider } from './git/GitProvider' import { CodacyCloud } from './git/CodacyCloud' import { PullRequestSummaryTree } from './views/PullRequestSummaryTree' @@ -221,6 +222,15 @@ export async function activate(context: vscode.ExtensionContext) { context.globalState.update('codacy.hasBeenActivated', true) } + // Re-apply proxy settings when the VS Code http configuration changes + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('http')) { + configureAxiosProxy() + } + }) + ) + // Listen for workspace folder changes context.subscriptions.push( vscode.workspace.onDidChangeWorkspaceFolders(() => { From 84c87137b041ec4664f232d8f54c92593525ff7e Mon Sep 17 00:00:00 2001 From: Neda Kaighobadi Date: Tue, 26 May 2026 11:12:18 +0300 Subject: [PATCH 2/6] fix proxy for http connection --- package-lock.json | 11 +++++++++++ package.json | 3 ++- src/common/proxy.ts | 36 +++++++++++++++++++++++++++--------- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index d479141..ea4e04a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/sarif": "2.1.7", "@types/sinon": "^10.0.16", "@types/temp": "^0.9.1", + "@types/tunnel": "^0.0.7", "@types/vscode": "^1.102.0", "@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/parser": "^6.2.1", @@ -998,6 +999,16 @@ "@types/node": "*" } }, + "node_modules/@types/tunnel": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.7.tgz", + "integrity": "sha512-VYKjZSmb2PvUwXoux4Gy4LAk7kzOB1ktkjyL4lxvpkqL2adgR+Qrh/yFyWluvJgIXWFicqs7XuzPI2NbTO/r3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", diff --git a/package.json b/package.json index c36c3cc..0f2744c 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "title": "Install Local Analysis", "description": "Get instant feedback as you type by analyzing your code locally.\n\n[Install Codacy CLI](command:codacy.installCLI)\n\nInstalls all required dependencies: Node, Python, Java", "media": { - "image": { + "image": { "light": "resources/walkthrough/light/local-analysis.svg", "dark": "resources/walkthrough/dark/local-analysis.svg", "hc": "resources/walkthrough/dark/local-analysis.svg", @@ -586,6 +586,7 @@ "@types/sarif": "2.1.7", "@types/sinon": "^10.0.16", "@types/temp": "^0.9.1", + "@types/tunnel": "^0.0.7", "@types/vscode": "^1.102.0", "@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/parser": "^6.2.1", diff --git a/src/common/proxy.ts b/src/common/proxy.ts index 023780d..24b15f7 100644 --- a/src/common/proxy.ts +++ b/src/common/proxy.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode' -import { HttpsProxyAgent } from 'https-proxy-agent' +import * as tunnel from 'tunnel' import { HttpProxyAgent } from 'http-proxy-agent' import axios from 'axios' import type { HTTPClient, HTTPClientRequest, HTTPResponse } from '@segment/analytics-node' @@ -33,21 +33,39 @@ export function configureAxiosProxy(): void { if (proxyUrl) { const strictSSL = resolveStrictSSL() const authHeader = resolveProxyAuthorization() - const extraHeaders = authHeader ? { 'Proxy-Authorization': authHeader } : undefined - axios.defaults.httpsAgent = new HttpsProxyAgent(proxyUrl, { - rejectUnauthorized: strictSSL, - ...(extraHeaders ? { headers: extraHeaders } : {}), - }) - axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl, { - rejectUnauthorized: strictSSL, - }) + const parsed = new URL(proxyUrl) + const proxyHost = parsed.hostname + const proxyPort = parseInt(parsed.port || (parsed.protocol === 'https:' ? '443' : '80'), 10) + const proxyHeaders = authHeader ? { 'Proxy-Authorization': authHeader } : undefined + const proxyOpts = { host: proxyHost, port: proxyPort, ...(proxyHeaders ? { headers: proxyHeaders } : {}) } + + // Branch on the proxy protocol so TLS proxies work too. + if (parsed.protocol === 'https:') { + axios.defaults.httpsAgent = tunnel.httpsOverHttps({ rejectUnauthorized: strictSSL, proxy: proxyOpts }) + axios.defaults.httpAgent = tunnel.httpOverHttps({ proxy: proxyOpts }) + } else { + axios.defaults.httpsAgent = tunnel.httpsOverHttp({ rejectUnauthorized: strictSSL, proxy: proxyOpts }) + axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl) + } // Disable axios's built-in proxy parsing so the agents take full control axios.defaults.proxy = false + + // Per-connection rejectUnauthorized on the tunnel agent is not sufficient + // in all Node.js versions to suppress TLS errors from an intercepting + // proxy cert. Mirror the VS Code proxyStrictSSL setting via the + // process-level flag so it is reliably honoured. + if (!strictSSL) { + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' + } else { + delete process.env['NODE_TLS_REJECT_UNAUTHORIZED'] + } } else { axios.defaults.httpsAgent = undefined axios.defaults.httpAgent = undefined axios.defaults.proxy = undefined + // Restore TLS verification when proxy is removed or strictSSL is re-enabled + delete process.env['NODE_TLS_REJECT_UNAUTHORIZED'] } } From ef37153f4448b2cec054b07deb9a13ac6bd94086 Mon Sep 17 00:00:00 2001 From: Neda Kaighobadi Date: Tue, 26 May 2026 11:25:05 +0300 Subject: [PATCH 3/6] add test for the proxy config --- src/test/suite/common/proxy.test.ts | 193 ++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 src/test/suite/common/proxy.test.ts diff --git a/src/test/suite/common/proxy.test.ts b/src/test/suite/common/proxy.test.ts new file mode 100644 index 0000000..822ff45 --- /dev/null +++ b/src/test/suite/common/proxy.test.ts @@ -0,0 +1,193 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as assert from 'assert' +import * as http from 'http' +import * as https from 'https' +import { SinonSandbox, createSandbox } from 'sinon' +import * as vscode from 'vscode' +import axios from 'axios' +import { HttpProxyAgent } from 'http-proxy-agent' +import { configureAxiosProxy } from '../../../common/proxy' + +function makeHttpConfig(opts: { proxy?: string; proxyStrictSSL?: boolean; proxyAuthorization?: string | null }) { + return { + get: (key: string, defaultValue?: T): T | undefined => { + if (key === 'proxy') return opts.proxy as unknown as T + if (key === 'proxyStrictSSL') return (opts.proxyStrictSSL !== undefined ? opts.proxyStrictSSL : defaultValue) as T + if (key === 'proxyAuthorization') return (opts.proxyAuthorization ?? undefined) as T + return defaultValue + }, + has: () => false, + inspect: () => undefined, + update: async () => {}, + } as unknown as vscode.WorkspaceConfiguration +} + +suite('configureAxiosProxy', () => { + let sandbox: SinonSandbox + let savedEnv: Record + + setup(() => { + sandbox = createSandbox() + savedEnv = { + NODE_TLS_REJECT_UNAUTHORIZED: process.env['NODE_TLS_REJECT_UNAUTHORIZED'], + HTTPS_PROXY: process.env['HTTPS_PROXY'], + https_proxy: process.env['https_proxy'], + HTTP_PROXY: process.env['HTTP_PROXY'], + http_proxy: process.env['http_proxy'], + } + // Start clean + delete process.env['NODE_TLS_REJECT_UNAUTHORIZED'] + delete process.env['HTTPS_PROXY'] + delete process.env['https_proxy'] + delete process.env['HTTP_PROXY'] + delete process.env['http_proxy'] + }) + + teardown(() => { + sandbox.restore() + axios.defaults.httpsAgent = undefined + axios.defaults.httpAgent = undefined + axios.defaults.proxy = undefined + // Restore env vars + for (const [k, v] of Object.entries(savedEnv)) { + if (v === undefined) { + delete process.env[k] + } else { + process.env[k] = v + } + } + }) + + function stubConfig(opts: { proxy?: string; proxyStrictSSL?: boolean; proxyAuthorization?: string | null }) { + sandbox.stub(vscode.workspace, 'getConfiguration').callsFake((section?: string) => { + if (section === 'http') return makeHttpConfig(opts) + // proxy.ts only calls getConfiguration('http'); return a no-op for anything else + return makeHttpConfig({}) + }) + } + + test('sets httpsOverHttp and HttpProxyAgent for an http:// proxy', () => { + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.ok(httpsAgent, 'httpsAgent must be set') + assert.strictEqual(httpsAgent.request, http.request, 'httpsAgent should be a httpsOverHttp tunnel agent') + assert.strictEqual(httpsAgent.options.proxy.host, 'proxy.example.com') + assert.strictEqual(httpsAgent.options.proxy.port, 8080) + + assert.ok(axios.defaults.httpAgent instanceof HttpProxyAgent, 'httpAgent should be an HttpProxyAgent') + assert.strictEqual(axios.defaults.proxy, false, 'axios built-in proxy parsing must be disabled') + }) + + test('sets httpsOverHttps and httpOverHttps for an https:// proxy', () => { + stubConfig({ proxy: 'https://proxy.example.com:8443' }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.ok(httpsAgent, 'httpsAgent must be set') + assert.strictEqual(httpsAgent.request, https.request, 'httpsAgent should be a httpsOverHttps tunnel agent') + assert.strictEqual(httpsAgent.options.proxy.host, 'proxy.example.com') + assert.strictEqual(httpsAgent.options.proxy.port, 8443) + + const httpAgent = axios.defaults.httpAgent as any + assert.ok(httpAgent, 'httpAgent must be set') + assert.strictEqual(httpAgent.request, https.request, 'httpAgent should be a httpOverHttps tunnel agent') + assert.strictEqual(axios.defaults.proxy, false) + }) + + test('sets NODE_TLS_REJECT_UNAUTHORIZED=0 when proxyStrictSSL is false', () => { + stubConfig({ proxy: 'http://proxy.example.com:8080', proxyStrictSSL: false }) + configureAxiosProxy() + + assert.strictEqual(process.env['NODE_TLS_REJECT_UNAUTHORIZED'], '0') + const httpsAgent = axios.defaults.httpsAgent as any + assert.strictEqual(httpsAgent.options.rejectUnauthorized, false) + }) + + test('clears NODE_TLS_REJECT_UNAUTHORIZED when proxyStrictSSL is true', () => { + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' + stubConfig({ proxy: 'http://proxy.example.com:8080', proxyStrictSSL: true }) + configureAxiosProxy() + + assert.strictEqual(process.env['NODE_TLS_REJECT_UNAUTHORIZED'], undefined) + const httpsAgent = axios.defaults.httpsAgent as any + assert.strictEqual(httpsAgent.options.rejectUnauthorized, true) + }) + + test('includes Proxy-Authorization header when proxyAuthorization is set', () => { + stubConfig({ proxy: 'http://proxy.example.com:8080', proxyAuthorization: 'Basic dXNlcjpwYXNz' }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.deepStrictEqual(httpsAgent.options.proxy.headers, { 'Proxy-Authorization': 'Basic dXNlcjpwYXNz' }) + }) + + test('no proxy headers when proxyAuthorization is not set', () => { + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.strictEqual(httpsAgent.options.proxy.headers, undefined) + }) + + test('clears agents and env var when proxy is not configured', () => { + axios.defaults.httpsAgent = {} as any + axios.defaults.httpAgent = {} as any + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0' + + stubConfig({ proxy: undefined }) + configureAxiosProxy() + + assert.strictEqual(axios.defaults.httpsAgent, undefined) + assert.strictEqual(axios.defaults.httpAgent, undefined) + assert.strictEqual(axios.defaults.proxy, undefined) + assert.strictEqual(process.env['NODE_TLS_REJECT_UNAUTHORIZED'], undefined) + }) + + test('empty VS Code proxy string is treated as no proxy', () => { + stubConfig({ proxy: '' }) + configureAxiosProxy() + + assert.strictEqual(axios.defaults.httpsAgent, undefined) + assert.strictEqual(axios.defaults.httpAgent, undefined) + }) + + test('falls back to HTTPS_PROXY env var when VS Code proxy is undefined', () => { + process.env['HTTPS_PROXY'] = 'http://envproxy.example.com:3128' + stubConfig({ proxy: undefined }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.ok(httpsAgent, 'httpsAgent must be set from HTTPS_PROXY env var') + assert.strictEqual(httpsAgent.options.proxy.host, 'envproxy.example.com') + assert.strictEqual(httpsAgent.options.proxy.port, 3128) + assert.strictEqual(axios.defaults.proxy, false) + }) + + test('VS Code proxy setting takes precedence over HTTPS_PROXY env var', () => { + process.env['HTTPS_PROXY'] = 'http://envproxy.example.com:3128' + stubConfig({ proxy: 'http://vscodeproxy.example.com:8080' }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.strictEqual(httpsAgent.options.proxy.host, 'vscodeproxy.example.com') + assert.strictEqual(httpsAgent.options.proxy.port, 8080) + }) + + test('uses default port 80 when http:// proxy has no explicit port', () => { + stubConfig({ proxy: 'http://proxy.example.com' }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.strictEqual(httpsAgent.options.proxy.port, 80) + }) + + test('uses default port 443 when https:// proxy has no explicit port', () => { + stubConfig({ proxy: 'https://proxy.example.com' }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.strictEqual(httpsAgent.options.proxy.port, 443) + }) +}) From 8553e39b3230065fb41026cce317dfa0ae86eea5 Mon Sep 17 00:00:00 2001 From: Neda Kaighobadi Date: Tue, 26 May 2026 11:34:21 +0300 Subject: [PATCH 4/6] implement suggestions --- src/common/proxy.ts | 1 + src/extension.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/common/proxy.ts b/src/common/proxy.ts index 24b15f7..a15c9c5 100644 --- a/src/common/proxy.ts +++ b/src/common/proxy.ts @@ -78,6 +78,7 @@ export class ProxiedSegmentHTTPClient implements HTTPClient { const response = await axios.post(options.url, options.data, { headers: options.headers, timeout: options.httpRequestTimeout, + validateStatus: () => true, }) return { status: response.status, statusText: response.statusText } } diff --git a/src/extension.ts b/src/extension.ts index d72812e..8d47450 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -210,6 +210,9 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(new SupportTree(context)) const setupViewProvider = activateWebview(context) + // Apply proxy settings before any outbound calls (telemetry, API, etc.) + configureAxiosProxy() + // Initialize telemetry with anonymous ID Telemetry.init(context) From 7852bafe01a3e9e005f31eea611a78c71ac1fd2f Mon Sep 17 00:00:00 2001 From: Neda Kaighobadi Date: Tue, 26 May 2026 12:02:43 +0300 Subject: [PATCH 5/6] implement no_proxy functionality --- src/common/proxy.ts | 52 +++++++++++- src/test/suite/common/proxy.test.ts | 125 +++++++++++++++++++++++++++- 2 files changed, 172 insertions(+), 5 deletions(-) diff --git a/src/common/proxy.ts b/src/common/proxy.ts index a15c9c5..f221abb 100644 --- a/src/common/proxy.ts +++ b/src/common/proxy.ts @@ -24,17 +24,53 @@ function resolveProxyAuthorization(): string | undefined { return vscode.workspace.getConfiguration('http').get('proxyAuthorization') ?? undefined } +function resolveNoProxy(): string[] { + const raw = process.env.NO_PROXY || process.env.no_proxy || '' + return raw + .split(',') + .map((s) => s.trim()) + .filter(Boolean) +} + +function hostMatchesNoProxy(hostname: string, entry: string): boolean { + if (entry === '*') return true + const normalized = entry.startsWith('.') ? entry.slice(1) : entry + return hostname === normalized || hostname.endsWith('.' + normalized) +} + +function shouldBypassProxy(url: string, noProxyList: string[]): boolean { + try { + const { hostname } = new URL(url) + return noProxyList.some((entry) => hostMatchesNoProxy(hostname, entry)) + } catch { + return false + } +} + +// Ensures the URL has a protocol so `new URL()` doesn't throw for bare host:port strings +function normalizeProxyUrl(url: string): string { + return url.includes('://') ? url : `http://${url}` +} + +let noProxyInterceptorId: number | null = null + /** * Applies proxy settings from VS Code config and environment variables to the global axios instance */ export function configureAxiosProxy(): void { + if (noProxyInterceptorId !== null) { + axios.interceptors.request.eject(noProxyInterceptorId) + noProxyInterceptorId = null + } + const proxyUrl = resolveProxyUrl() if (proxyUrl) { const strictSSL = resolveStrictSSL() const authHeader = resolveProxyAuthorization() - const parsed = new URL(proxyUrl) + const normalizedUrl = normalizeProxyUrl(proxyUrl) + const parsed = new URL(normalizedUrl) const proxyHost = parsed.hostname const proxyPort = parseInt(parsed.port || (parsed.protocol === 'https:' ? '443' : '80'), 10) const proxyHeaders = authHeader ? { 'Proxy-Authorization': authHeader } : undefined @@ -46,7 +82,7 @@ export function configureAxiosProxy(): void { axios.defaults.httpAgent = tunnel.httpOverHttps({ proxy: proxyOpts }) } else { axios.defaults.httpsAgent = tunnel.httpsOverHttp({ rejectUnauthorized: strictSSL, proxy: proxyOpts }) - axios.defaults.httpAgent = new HttpProxyAgent(proxyUrl) + axios.defaults.httpAgent = new HttpProxyAgent(normalizedUrl) } // Disable axios's built-in proxy parsing so the agents take full control axios.defaults.proxy = false @@ -60,6 +96,18 @@ export function configureAxiosProxy(): void { } else { delete process.env['NODE_TLS_REJECT_UNAUTHORIZED'] } + + const noProxyList = resolveNoProxy() + if (noProxyList.length > 0) { + noProxyInterceptorId = axios.interceptors.request.use((config) => { + if (config.url && shouldBypassProxy(config.url, noProxyList)) { + config.httpsAgent = undefined + config.httpAgent = undefined + config.proxy = undefined + } + return config + }) + } } else { axios.defaults.httpsAgent = undefined axios.defaults.httpAgent = undefined diff --git a/src/test/suite/common/proxy.test.ts b/src/test/suite/common/proxy.test.ts index 822ff45..fd7fb8f 100644 --- a/src/test/suite/common/proxy.test.ts +++ b/src/test/suite/common/proxy.test.ts @@ -25,22 +25,27 @@ function makeHttpConfig(opts: { proxy?: string; proxyStrictSSL?: boolean; proxyA suite('configureAxiosProxy', () => { let sandbox: SinonSandbox let savedEnv: Record + let initialInterceptorHandlerCount: number setup(() => { sandbox = createSandbox() + initialInterceptorHandlerCount = (axios.interceptors.request as any).handlers.length savedEnv = { NODE_TLS_REJECT_UNAUTHORIZED: process.env['NODE_TLS_REJECT_UNAUTHORIZED'], HTTPS_PROXY: process.env['HTTPS_PROXY'], https_proxy: process.env['https_proxy'], HTTP_PROXY: process.env['HTTP_PROXY'], http_proxy: process.env['http_proxy'], + NO_PROXY: process.env['NO_PROXY'], + no_proxy: process.env['no_proxy'], } - // Start clean delete process.env['NODE_TLS_REJECT_UNAUTHORIZED'] delete process.env['HTTPS_PROXY'] delete process.env['https_proxy'] delete process.env['HTTP_PROXY'] delete process.env['http_proxy'] + delete process.env['NO_PROXY'] + delete process.env['no_proxy'] }) teardown(() => { @@ -48,7 +53,13 @@ suite('configureAxiosProxy', () => { axios.defaults.httpsAgent = undefined axios.defaults.httpAgent = undefined axios.defaults.proxy = undefined - // Restore env vars + // Eject any interceptors added during this test + const handlers = (axios.interceptors.request as any).handlers as Array + for (let i = initialInterceptorHandlerCount; i < handlers.length; i++) { + if (handlers[i] !== null) { + axios.interceptors.request.eject(i) + } + } for (const [k, v] of Object.entries(savedEnv)) { if (v === undefined) { delete process.env[k] @@ -61,11 +72,21 @@ suite('configureAxiosProxy', () => { function stubConfig(opts: { proxy?: string; proxyStrictSSL?: boolean; proxyAuthorization?: string | null }) { sandbox.stub(vscode.workspace, 'getConfiguration').callsFake((section?: string) => { if (section === 'http') return makeHttpConfig(opts) - // proxy.ts only calls getConfiguration('http'); return a no-op for anything else return makeHttpConfig({}) }) } + function activeInterceptorCount(): number { + return ((axios.interceptors.request as any).handlers as Array).filter(Boolean).length + } + + function getLastInterceptorFn(): ((config: any) => any) | undefined { + const handlers = (axios.interceptors.request as any).handlers as Array<{ fulfilled: (c: any) => any } | null> + return [...handlers].reverse().find(Boolean)?.fulfilled + } + + // --- proxy agent setup --- + test('sets httpsOverHttp and HttpProxyAgent for an http:// proxy', () => { stubConfig({ proxy: 'http://proxy.example.com:8080' }) configureAxiosProxy() @@ -190,4 +211,102 @@ suite('configureAxiosProxy', () => { const httpsAgent = axios.defaults.httpsAgent as any assert.strictEqual(httpsAgent.options.proxy.port, 443) }) + + test('normalizes a protocol-less proxy URL before parsing', () => { + stubConfig({ proxy: 'proxy.example.com:8080' }) + configureAxiosProxy() + + const httpsAgent = axios.defaults.httpsAgent as any + assert.ok(httpsAgent, 'httpsAgent must be set for a bare host:port proxy') + assert.strictEqual(httpsAgent.options.proxy.host, 'proxy.example.com') + assert.strictEqual(httpsAgent.options.proxy.port, 8080) + }) + + // --- NO_PROXY --- + + test('registers a request interceptor when NO_PROXY is set', () => { + process.env['NO_PROXY'] = 'internal.example.com' + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + const before = activeInterceptorCount() + configureAxiosProxy() + + assert.strictEqual(activeInterceptorCount(), before + 1) + }) + + test('interceptor clears agents for a host that matches NO_PROXY', () => { + process.env['NO_PROXY'] = 'internal.example.com' + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + configureAxiosProxy() + + const fn = getLastInterceptorFn()! + const config = { url: 'https://internal.example.com/api', httpsAgent: {}, httpAgent: {}, proxy: false as const } + const result = fn(config) + + assert.strictEqual(result.httpsAgent, undefined) + assert.strictEqual(result.httpAgent, undefined) + assert.strictEqual(result.proxy, undefined) + }) + + test('interceptor leaves agents intact for a host that does not match NO_PROXY', () => { + process.env['NO_PROXY'] = 'internal.example.com' + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + configureAxiosProxy() + + const fn = getLastInterceptorFn()! + const agent = {} + const config = { url: 'https://app.codacy.com/api', httpsAgent: agent, httpAgent: agent, proxy: false as const } + const result = fn(config) + + assert.strictEqual(result.httpsAgent, agent) + assert.strictEqual(result.httpAgent, agent) + }) + + test('interceptor bypasses proxy for subdomains of a NO_PROXY entry', () => { + process.env['NO_PROXY'] = '.example.com' + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + configureAxiosProxy() + + const fn = getLastInterceptorFn()! + const result = fn({ url: 'https://sub.example.com/api', httpsAgent: {}, proxy: false as const }) + + assert.strictEqual(result.httpsAgent, undefined) + }) + + test('interceptor bypasses proxy for all hosts when NO_PROXY is *', () => { + process.env['NO_PROXY'] = '*' + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + configureAxiosProxy() + + const fn = getLastInterceptorFn()! + const result = fn({ url: 'https://anything.com/api', httpsAgent: {}, proxy: false as const }) + + assert.strictEqual(result.httpsAgent, undefined) + }) + + test('no_proxy lowercase env var is respected', () => { + process.env['no_proxy'] = 'internal.example.com' + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + const before = activeInterceptorCount() + configureAxiosProxy() + + assert.strictEqual(activeInterceptorCount(), before + 1) + }) + + test('no interceptor registered when NO_PROXY is not set', () => { + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + const before = activeInterceptorCount() + configureAxiosProxy() + + assert.strictEqual(activeInterceptorCount(), before) + }) + + test('re-calling configureAxiosProxy replaces the NO_PROXY interceptor rather than accumulating', () => { + process.env['NO_PROXY'] = 'internal.example.com' + stubConfig({ proxy: 'http://proxy.example.com:8080' }) + const before = activeInterceptorCount() + configureAxiosProxy() + configureAxiosProxy() + + assert.strictEqual(activeInterceptorCount(), before + 1) + }) }) From e8d70d6ce3cc23be14ac33413f893c4fc1f85295 Mon Sep 17 00:00:00 2001 From: Neda Kaighobadi Date: Tue, 26 May 2026 16:18:32 +0300 Subject: [PATCH 6/6] remove redundant checks --- src/common/proxy.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/common/proxy.ts b/src/common/proxy.ts index f221abb..fe86519 100644 --- a/src/common/proxy.ts +++ b/src/common/proxy.ts @@ -8,12 +8,10 @@ function resolveProxyUrl(): string | undefined { // VS Code setting takes precedence; an empty string means "no proxy" const vscodeProxy = vscode.workspace.getConfiguration('http').get('proxy') if (vscodeProxy !== undefined) { - return vscodeProxy || undefined + return vscodeProxy } - return ( - process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || undefined - ) + return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy } function resolveStrictSSL(): boolean {