From 1203fabaccc62d092747f0b855079453846fcbcc Mon Sep 17 00:00:00 2001 From: joelwizard Date: Fri, 3 Apr 2026 14:46:00 -0700 Subject: [PATCH 1/6] Do not pass proxy headers to the proxy agent --- dist/merge/index.js | 12 +++++++++--- dist/upload/index.js | 12 +++++++++--- package-lock.json | 13 +++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/dist/merge/index.js b/dist/merge/index.js index 3b522d4..a3022f3 100644 --- a/dist/merge/index.js +++ b/dist/merge/index.js @@ -89375,16 +89375,22 @@ function setProxyAgentOnRequest(request, cachedAgents, proxyUrl) { if (request.tlsSettings) { log_logger.warning("TLS settings are not supported in combination with custom Proxy, certificates provided to the client will be ignored."); } - const headers = request.headers.toJSON(); + // Do NOT pass application-level request headers to the proxy agent. + // The `headers` option in HttpsProxyAgent/HttpProxyAgent specifies headers + // to include in the HTTP CONNECT request to the proxy server. Leaking + // application headers (Content-Type, x-ms-version, etc.) into the CONNECT + // handshake violates RFC 7231 §4.3.6 and causes strict proxies (e.g. + // Fortinet, Zscaler) to reject the tunnel, resulting in ECONNRESET. + // See: https://github.com/actions/upload-artifact/issues/XXX if (isInsecure) { if (!cachedAgents.httpProxyAgent) { - cachedAgents.httpProxyAgent = new http_proxy_agent_dist.HttpProxyAgent(proxyUrl, { headers }); + cachedAgents.httpProxyAgent = new http_proxy_agent_dist.HttpProxyAgent(proxyUrl); } request.agent = cachedAgents.httpProxyAgent; } else { if (!cachedAgents.httpsProxyAgent) { - cachedAgents.httpsProxyAgent = new dist.HttpsProxyAgent(proxyUrl, { headers }); + cachedAgents.httpsProxyAgent = new dist.HttpsProxyAgent(proxyUrl); } request.agent = cachedAgents.httpsProxyAgent; } diff --git a/dist/upload/index.js b/dist/upload/index.js index a4ca651..c189d3c 100644 --- a/dist/upload/index.js +++ b/dist/upload/index.js @@ -86950,16 +86950,22 @@ function setProxyAgentOnRequest(request, cachedAgents, proxyUrl) { if (request.tlsSettings) { log_logger.warning("TLS settings are not supported in combination with custom Proxy, certificates provided to the client will be ignored."); } - const headers = request.headers.toJSON(); + // Do NOT pass application-level request headers to the proxy agent. + // The `headers` option in HttpsProxyAgent/HttpProxyAgent specifies headers + // to include in the HTTP CONNECT request to the proxy server. Leaking + // application headers (Content-Type, x-ms-version, etc.) into the CONNECT + // handshake violates RFC 7231 §4.3.6 and causes strict proxies (e.g. + // Fortinet, Zscaler) to reject the tunnel, resulting in ECONNRESET. + // See: https://github.com/actions/upload-artifact/issues/XXX if (isInsecure) { if (!cachedAgents.httpProxyAgent) { - cachedAgents.httpProxyAgent = new http_proxy_agent_dist.HttpProxyAgent(proxyUrl, { headers }); + cachedAgents.httpProxyAgent = new http_proxy_agent_dist.HttpProxyAgent(proxyUrl); } request.agent = cachedAgents.httpProxyAgent; } else { if (!cachedAgents.httpsProxyAgent) { - cachedAgents.httpsProxyAgent = new dist.HttpsProxyAgent(proxyUrl, { headers }); + cachedAgents.httpsProxyAgent = new dist.HttpsProxyAgent(proxyUrl); } request.agent = cachedAgents.httpsProxyAgent; } diff --git a/package-lock.json b/package-lock.json index 5739b1d..7f3030a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -188,6 +188,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -249,6 +250,7 @@ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "license": "MIT", + "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -390,6 +392,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1799,6 +1802,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -2203,6 +2207,7 @@ "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -2794,6 +2799,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3387,6 +3393,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4272,6 +4279,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6285,6 +6293,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -7757,6 +7766,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8808,6 +8818,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8939,6 +8950,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9147,6 +9159,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From ba6bdbd8974e3c6100f6abc39714ce180f76a502 Mon Sep 17 00:00:00 2001 From: joelwizard Date: Fri, 3 Apr 2026 15:01:34 -0700 Subject: [PATCH 2/6] Add proxy tests --- __tests__/proxy-policy.test.ts | 197 +++++++++++++++++++++++++++++++++ package-lock.json | 13 --- 2 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 __tests__/proxy-policy.test.ts diff --git a/__tests__/proxy-policy.test.ts b/__tests__/proxy-policy.test.ts new file mode 100644 index 0000000..b02684f --- /dev/null +++ b/__tests__/proxy-policy.test.ts @@ -0,0 +1,197 @@ +/** + * Tests that the proxyPolicy does NOT leak application-level request headers + * into the HTTP CONNECT tunnel handshake. + * + * Background: HttpsProxyAgent's `headers` constructor option specifies headers + * to send in the CONNECT request to the proxy server. When Azure SDK request + * headers (Content-Type, x-ms-version, x-ms-blob-type, etc.) are passed here, + * strict corporate proxies (Fortinet, Zscaler) reject the tunnel — causing + * ECONNRESET. See: https://github.com/actions/upload-artifact/issues/XXX + */ +import {describe, test, expect, beforeEach, afterEach} from '@jest/globals' + +import { + createPipelineRequest, + type PipelineRequest, + type SendRequest +} from '@typespec/ts-http-runtime' +import {proxyPolicy} from '@typespec/ts-http-runtime/internal/policies' +import {HttpsProxyAgent} from 'https-proxy-agent' +import {HttpProxyAgent} from 'http-proxy-agent' + +describe('proxyPolicy', () => { + const PROXY_URL = 'http://corporate-proxy.example.com:3128' + + let savedHttpsProxy: string | undefined + let savedHttpProxy: string | undefined + let savedNoProxy: string | undefined + + beforeEach(() => { + // Save and set proxy env vars + savedHttpsProxy = process.env['HTTPS_PROXY'] + savedHttpProxy = process.env['HTTP_PROXY'] + savedNoProxy = process.env['NO_PROXY'] + + process.env['HTTPS_PROXY'] = PROXY_URL + process.env['HTTP_PROXY'] = PROXY_URL + delete process.env['NO_PROXY'] + }) + + afterEach(() => { + // Restore original env + if (savedHttpsProxy !== undefined) { + process.env['HTTPS_PROXY'] = savedHttpsProxy + } else { + delete process.env['HTTPS_PROXY'] + } + if (savedHttpProxy !== undefined) { + process.env['HTTP_PROXY'] = savedHttpProxy + } else { + delete process.env['HTTP_PROXY'] + } + if (savedNoProxy !== undefined) { + process.env['NO_PROXY'] = savedNoProxy + } else { + delete process.env['NO_PROXY'] + } + }) + + /** + * A mock "next" handler that captures the request after the proxy policy + * has set the agent, so we can inspect it. + */ + function createCapturingNext(): SendRequest & { + capturedRequest: PipelineRequest | undefined + } { + const fn = async (request: PipelineRequest) => { + fn.capturedRequest = request + return { + status: 200, + headers: createPipelineRequest({url: ''}).headers, + request + } + } + fn.capturedRequest = undefined as PipelineRequest | undefined + return fn + } + + test('does not leak application headers into HttpsProxyAgent CONNECT request', async () => { + const policy = proxyPolicy() + const next = createCapturingNext() + + // Simulate an Azure Blob Storage upload request with typical SDK headers + const request = createPipelineRequest({ + url: 'https://productionresultssa0.blob.core.windows.net/artifacts/upload' + }) + request.headers.set('Content-Type', 'application/octet-stream') + request.headers.set('x-ms-version', '2024-11-04') + request.headers.set('x-ms-blob-type', 'BlockBlob') + request.headers.set( + 'x-ms-client-request-id', + '00000000-0000-0000-0000-000000000000' + ) + + await policy.sendRequest(request, next) + + // The policy should have assigned an HttpsProxyAgent + const agent = next.capturedRequest?.agent + expect(agent).toBeDefined() + expect(agent).toBeInstanceOf(HttpsProxyAgent) + + // CRITICAL: The agent's proxyHeaders must NOT contain application headers. + // If this fails, application headers are being leaked into the CONNECT + // request, which breaks strict corporate proxies. + const proxyAgent = agent as HttpsProxyAgent + const proxyHeaders = + typeof proxyAgent.proxyHeaders === 'function' + ? proxyAgent.proxyHeaders() + : proxyAgent.proxyHeaders + + expect(proxyHeaders).toBeDefined() + + // None of the Azure SDK application headers should appear + const headerObj = proxyHeaders as Record + expect(headerObj['content-type']).toBeUndefined() + expect(headerObj['Content-Type']).toBeUndefined() + expect(headerObj['x-ms-version']).toBeUndefined() + expect(headerObj['x-ms-blob-type']).toBeUndefined() + expect(headerObj['x-ms-client-request-id']).toBeUndefined() + + // proxyHeaders should be empty (no application headers leaked) + expect(Object.keys(headerObj).length).toBe(0) + }) + + test('does not leak application headers into HttpProxyAgent CONNECT request', async () => { + const policy = proxyPolicy() + const next = createCapturingNext() + + // Simulate an insecure (HTTP) request with application headers + const request = createPipelineRequest({ + url: 'http://example.com/api/upload', + allowInsecureConnection: true + }) + request.headers.set('Content-Type', 'application/json') + request.headers.set('Authorization', 'Bearer some-token') + + await policy.sendRequest(request, next) + + const agent = next.capturedRequest?.agent + expect(agent).toBeDefined() + expect(agent).toBeInstanceOf(HttpProxyAgent) + }) + + test('still routes HTTPS requests through the proxy', async () => { + const policy = proxyPolicy() + const next = createCapturingNext() + + const request = createPipelineRequest({ + url: 'https://results-receiver.actions.githubusercontent.com/twirp/test' + }) + + await policy.sendRequest(request, next) + + const agent = next.capturedRequest?.agent + expect(agent).toBeDefined() + expect(agent).toBeInstanceOf(HttpsProxyAgent) + + // Verify the proxy URL is correct + const proxyAgent = agent as HttpsProxyAgent + expect(proxyAgent.proxy.href).toBe(`${PROXY_URL}/`) + }) + + test('bypasses proxy for no_proxy hosts', async () => { + // Use customNoProxyList since globalNoProxyList is only loaded once. + // Patterns starting with "." match subdomains (e.g. ".example.com" + // matches "api.example.com"), bare names match the host exactly. + const policy = proxyPolicy(undefined, { + customNoProxyList: ['.blob.core.windows.net', 'exact-host.test'] + }) + const next = createCapturingNext() + + // This host matches ".blob.core.windows.net" via subdomain matching + const request = createPipelineRequest({ + url: 'https://productionresultssa0.blob.core.windows.net/artifacts/upload' + }) + + await policy.sendRequest(request, next) + + // Agent should not be set for a bypassed host + expect(next.capturedRequest?.agent).toBeUndefined() + }) + + test('does not override a custom agent already set on the request', async () => { + const policy = proxyPolicy() + const next = createCapturingNext() + + const customAgent = new HttpsProxyAgent('http://custom-proxy:9999') + const request = createPipelineRequest({ + url: 'https://blob.core.windows.net/test' + }) + request.agent = customAgent + + await policy.sendRequest(request, next) + + // The policy should not overwrite the pre-existing agent + expect(next.capturedRequest?.agent).toBe(customAgent) + }) +}) diff --git a/package-lock.json b/package-lock.json index 7f3030a..5739b1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -188,7 +188,6 @@ "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "license": "MIT", - "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -250,7 +249,6 @@ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", "license": "MIT", - "peer": true, "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", @@ -392,7 +390,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1802,7 +1799,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -2207,7 +2203,6 @@ "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -2799,7 +2794,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3393,7 +3387,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4279,7 +4272,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6293,7 +6285,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -7766,7 +7757,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8818,7 +8808,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8950,7 +8939,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9159,7 +9147,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From 7a137977ba36ea58b05e66e66a0c941c9f86b8a3 Mon Sep 17 00:00:00 2001 From: joelwizard Date: Fri, 3 Apr 2026 15:04:44 -0700 Subject: [PATCH 3/6] Correct the comment to point to the right issue --- __tests__/proxy-policy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/proxy-policy.test.ts b/__tests__/proxy-policy.test.ts index b02684f..e6cccde 100644 --- a/__tests__/proxy-policy.test.ts +++ b/__tests__/proxy-policy.test.ts @@ -6,7 +6,7 @@ * to send in the CONNECT request to the proxy server. When Azure SDK request * headers (Content-Type, x-ms-version, x-ms-blob-type, etc.) are passed here, * strict corporate proxies (Fortinet, Zscaler) reject the tunnel — causing - * ECONNRESET. See: https://github.com/actions/upload-artifact/issues/XXX + * ECONNRESET. See: https://github.com/actions/upload-artifact/issues/747 */ import {describe, test, expect, beforeEach, afterEach} from '@jest/globals' From d4973cc41aa38be3a4b04cf564277145644c1383 Mon Sep 17 00:00:00 2001 From: Joel Williams Date: Fri, 3 Apr 2026 15:35:25 -0700 Subject: [PATCH 4/6] Update __tests__/proxy-policy.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- __tests__/proxy-policy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/proxy-policy.test.ts b/__tests__/proxy-policy.test.ts index e6cccde..e00f9d1 100644 --- a/__tests__/proxy-policy.test.ts +++ b/__tests__/proxy-policy.test.ts @@ -12,10 +12,10 @@ import {describe, test, expect, beforeEach, afterEach} from '@jest/globals' import { createPipelineRequest, + proxyPolicy, type PipelineRequest, type SendRequest } from '@typespec/ts-http-runtime' -import {proxyPolicy} from '@typespec/ts-http-runtime/internal/policies' import {HttpsProxyAgent} from 'https-proxy-agent' import {HttpProxyAgent} from 'http-proxy-agent' From aef0e9ddf900b2f6cd368b47490e1de62115709f Mon Sep 17 00:00:00 2001 From: joelwizard Date: Fri, 3 Apr 2026 15:55:05 -0700 Subject: [PATCH 5/6] Make the tests both proxy cased and redo the import --- __tests__/proxy-policy.test.ts | 50 +++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/__tests__/proxy-policy.test.ts b/__tests__/proxy-policy.test.ts index e00f9d1..5331557 100644 --- a/__tests__/proxy-policy.test.ts +++ b/__tests__/proxy-policy.test.ts @@ -12,47 +12,53 @@ import {describe, test, expect, beforeEach, afterEach} from '@jest/globals' import { createPipelineRequest, - proxyPolicy, type PipelineRequest, type SendRequest } from '@typespec/ts-http-runtime' +import {proxyPolicy} from '@typespec/ts-http-runtime/internal/policies' import {HttpsProxyAgent} from 'https-proxy-agent' import {HttpProxyAgent} from 'http-proxy-agent' describe('proxyPolicy', () => { const PROXY_URL = 'http://corporate-proxy.example.com:3128' - let savedHttpsProxy: string | undefined - let savedHttpProxy: string | undefined - let savedNoProxy: string | undefined + // The runtime checks both uppercase and lowercase proxy env vars, so we + // must save/clear/restore both casings to keep tests hermetic. + const PROXY_ENV_KEYS = [ + 'HTTPS_PROXY', + 'https_proxy', + 'HTTP_PROXY', + 'http_proxy', + 'NO_PROXY', + 'no_proxy' + ] as const + + let savedEnv: Record beforeEach(() => { - // Save and set proxy env vars - savedHttpsProxy = process.env['HTTPS_PROXY'] - savedHttpProxy = process.env['HTTP_PROXY'] - savedNoProxy = process.env['NO_PROXY'] + // Save all proxy env vars + savedEnv = {} + for (const key of PROXY_ENV_KEYS) { + savedEnv[key] = process.env[key] + } + // Set uppercase, delete lowercase to avoid ambiguity process.env['HTTPS_PROXY'] = PROXY_URL process.env['HTTP_PROXY'] = PROXY_URL + delete process.env['https_proxy'] + delete process.env['http_proxy'] delete process.env['NO_PROXY'] + delete process.env['no_proxy'] }) afterEach(() => { // Restore original env - if (savedHttpsProxy !== undefined) { - process.env['HTTPS_PROXY'] = savedHttpsProxy - } else { - delete process.env['HTTPS_PROXY'] - } - if (savedHttpProxy !== undefined) { - process.env['HTTP_PROXY'] = savedHttpProxy - } else { - delete process.env['HTTP_PROXY'] - } - if (savedNoProxy !== undefined) { - process.env['NO_PROXY'] = savedNoProxy - } else { - delete process.env['NO_PROXY'] + for (const key of PROXY_ENV_KEYS) { + if (savedEnv[key] !== undefined) { + process.env[key] = savedEnv[key] + } else { + delete process.env[key] + } } }) From f88bc1e48989d270bc67336c883e0a21c92f8285 Mon Sep 17 00:00:00 2001 From: joelwizard Date: Fri, 3 Apr 2026 15:56:04 -0700 Subject: [PATCH 6/6] Rename the test more appropriately --- __tests__/proxy-policy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/proxy-policy.test.ts b/__tests__/proxy-policy.test.ts index 5331557..82cae80 100644 --- a/__tests__/proxy-policy.test.ts +++ b/__tests__/proxy-policy.test.ts @@ -127,7 +127,7 @@ describe('proxyPolicy', () => { expect(Object.keys(headerObj).length).toBe(0) }) - test('does not leak application headers into HttpProxyAgent CONNECT request', async () => { + test('selects HttpProxyAgent for plain HTTP requests without leaking headers', async () => { const policy = proxyPolicy() const next = createCapturingNext()