Skip to content

Commit d5d9ad6

Browse files
Adds NO_PROXY support for proxy configuration. Closes #7165
1 parent 537911c commit d5d9ad6

3 files changed

Lines changed: 202 additions & 1 deletion

File tree

docs/docs/user-guide/using-proxy-url.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,20 @@ When using a proxy, it's important to understand the different parts of a proxy
1515
- **username and password**: if the proxy server requires authentication, you will need to provide a username and password
1616
- **host**: the hostname or IP address of the proxy server
1717
- **port number**: the port number on which the proxy server is listening. Defaults to 443 for the `HTTPS` protocol, otherwise it defaults to 80 when not provided
18+
19+
## Bypassing the proxy for specific hosts
20+
21+
If you need certain requests to bypass the proxy, you can use the `NO_PROXY` (or `no_proxy`) environment variable. This is a comma-separated list of hostnames or domain patterns that should not be routed through the proxy.
22+
23+
Supported patterns:
24+
25+
- **Exact hostname**: `example.com` — matches only `example.com`
26+
- **Wildcard domain**: `*.example.com` — matches `sub.example.com` and `example.com`
27+
- **Dot-prefix domain**: `.example.com` — same behavior as `*.example.com`
28+
- **Wildcard all**: `*` — bypasses the proxy for all requests
29+
30+
For example, to bypass the proxy for Microsoft authentication endpoints:
31+
32+
```sh
33+
export NO_PROXY="*.microsoftonline.com,*.microsoft.com,*.azure.net,login.microsoftonline.com"
34+
```

src/request.spec.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,157 @@ describe('Request', () => {
377377
assert(proxyConfigured);
378378
});
379379

380+
it('bypasses proxy when NO_PROXY matches exact hostname', async () => {
381+
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080', 'NO_PROXY': 'contoso.sharepoint.com' });
382+
383+
sinon.stub(_request as any, 'req').callsFake((options) => {
384+
_options = options as CliRequestOptions;
385+
return { data: {} };
386+
});
387+
388+
await _request
389+
.get({
390+
url: 'https://contoso.sharepoint.com/'
391+
});
392+
393+
assert.strictEqual(_options.proxy, false);
394+
});
395+
396+
it('bypasses proxy when NO_PROXY matches wildcard domain', async () => {
397+
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080', 'NO_PROXY': '*.sharepoint.com' });
398+
399+
sinon.stub(_request as any, 'req').callsFake((options) => {
400+
_options = options as CliRequestOptions;
401+
return { data: {} };
402+
});
403+
404+
await _request
405+
.get({
406+
url: 'https://contoso.sharepoint.com/'
407+
});
408+
409+
assert.strictEqual(_options.proxy, false);
410+
});
411+
412+
it('bypasses proxy when NO_PROXY matches dot-prefix domain', async () => {
413+
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080', 'NO_PROXY': '.sharepoint.com' });
414+
415+
sinon.stub(_request as any, 'req').callsFake((options) => {
416+
_options = options as CliRequestOptions;
417+
return { data: {} };
418+
});
419+
420+
await _request
421+
.get({
422+
url: 'https://contoso.sharepoint.com/'
423+
});
424+
425+
assert.strictEqual(_options.proxy, false);
426+
});
427+
428+
it('bypasses proxy when NO_PROXY is wildcard *', async () => {
429+
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080', 'NO_PROXY': '*' });
430+
431+
sinon.stub(_request as any, 'req').callsFake((options) => {
432+
_options = options as CliRequestOptions;
433+
return { data: {} };
434+
});
435+
436+
await _request
437+
.get({
438+
url: 'https://contoso.sharepoint.com/'
439+
});
440+
441+
assert.strictEqual(_options.proxy, false);
442+
});
443+
444+
it('does not bypass proxy when NO_PROXY does not match hostname', async () => {
445+
let proxyConfigured = false;
446+
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080', 'NO_PROXY': '*.example.com' });
447+
448+
sinon.stub(_request as any, 'req').callsFake((options) => {
449+
_options = options as CliRequestOptions;
450+
proxyConfigured = !!_options.proxy &&
451+
_options.proxy.host === 'proxy.contoso.com' &&
452+
_options.proxy.port === 8080 &&
453+
_options.proxy.protocol === 'http';
454+
return { data: {} };
455+
});
456+
457+
await _request
458+
.get({
459+
url: 'https://contoso.sharepoint.com/'
460+
});
461+
462+
assert(proxyConfigured);
463+
});
464+
465+
it('bypasses proxy when NO_PROXY contains multiple entries and one matches', async () => {
466+
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080', 'NO_PROXY': 'example.com, *.sharepoint.com, localhost' });
467+
468+
sinon.stub(_request as any, 'req').callsFake((options) => {
469+
_options = options as CliRequestOptions;
470+
return { data: {} };
471+
});
472+
473+
await _request
474+
.get({
475+
url: 'https://contoso.sharepoint.com/'
476+
});
477+
478+
assert.strictEqual(_options.proxy, false);
479+
});
480+
481+
it('bypasses proxy with case-insensitive NO_PROXY matching', async () => {
482+
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080', 'NO_PROXY': '*.SharePoint.COM' });
483+
484+
sinon.stub(_request as any, 'req').callsFake((options) => {
485+
_options = options as CliRequestOptions;
486+
return { data: {} };
487+
});
488+
489+
await _request
490+
.get({
491+
url: 'https://contoso.sharepoint.com/'
492+
});
493+
494+
assert.strictEqual(_options.proxy, false);
495+
});
496+
497+
it('reads no_proxy (lowercase) environment variable', async () => {
498+
const env: Record<string, string> = { 'HTTPS_PROXY': 'http://proxy.contoso.com:8080' };
499+
env['no_proxy'] = 'contoso.sharepoint.com';
500+
sinon.stub(process, 'env').value(env);
501+
502+
sinon.stub(_request as any, 'req').callsFake((options) => {
503+
_options = options as CliRequestOptions;
504+
return { data: {} };
505+
});
506+
507+
await _request
508+
.get({
509+
url: 'https://contoso.sharepoint.com/'
510+
});
511+
512+
assert.strictEqual(_options.proxy, false);
513+
});
514+
515+
it('bypasses proxy when NO_PROXY wildcard matches the root domain exactly', async () => {
516+
sinon.stub(process, 'env').value({ 'HTTPS_PROXY': 'http://proxy.contoso.com:8080', 'NO_PROXY': '*.sharepoint.com' });
517+
518+
sinon.stub(_request as any, 'req').callsFake((options) => {
519+
_options = options as CliRequestOptions;
520+
return { data: {} };
521+
});
522+
523+
await _request
524+
.get({
525+
url: 'https://sharepoint.com/'
526+
});
527+
528+
assert.strictEqual(_options.proxy, false);
529+
});
530+
380531
it('correctly handles failed GET request', async () => {
381532
sinon.stub(_request as any, 'req').callsFake(options => {
382533
_options = options as CliRequestOptions;

src/request.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,12 @@ class Request {
189189
const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
190190

191191
if (proxyUrl) {
192-
options.proxy = this.createProxyConfigFromUrl(proxyUrl);
192+
if (this.shouldBypassProxy(options.url!)) {
193+
options.proxy = false;
194+
}
195+
else {
196+
options.proxy = this.createProxyConfigFromUrl(proxyUrl);
197+
}
193198
}
194199

195200
const res = await this.req(options);
@@ -236,6 +241,34 @@ class Request {
236241
options.url!.substring(8).replace('//', '/');
237242
}
238243

244+
private shouldBypassProxy(requestUrl: string): boolean {
245+
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
246+
247+
if (!noProxy) {
248+
return false;
249+
}
250+
251+
const hostname = new URL(requestUrl).hostname.toLowerCase();
252+
const entries = noProxy.split(',').map(e => e.trim().toLowerCase()).filter(e => e);
253+
254+
return entries.some(entry => {
255+
if (entry === '*') {
256+
return true;
257+
}
258+
259+
if (entry.startsWith('*.')) {
260+
const domain = entry.substring(1);
261+
return hostname.endsWith(domain) || hostname === entry.substring(2);
262+
}
263+
264+
if (entry.startsWith('.')) {
265+
return hostname.endsWith(entry) || hostname === entry.substring(1);
266+
}
267+
268+
return hostname === entry || hostname.endsWith('.' + entry);
269+
});
270+
}
271+
239272
private createProxyConfigFromUrl(url: string): AxiosProxyConfig {
240273
const parsedUrl = new URL(url);
241274
const port = parsedUrl.port || (url.toLowerCase().startsWith('https') ? 443 : 80);

0 commit comments

Comments
 (0)