New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add no_proxy env variable #361
Changes from 6 commits
9b60efd
5185e05
3d0bdd5
f2ab41a
3807cf2
472236d
e05f52c
00c27de
58f78a0
d433894
ea5fc09
a16855f
5a8f7c6
a8e02e4
32b7d6a
94a7f3a
2e9d695
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,8 +42,6 @@ function hasFetch() { | |
|
||
let HttpsProxyAgent: any; | ||
|
||
// Figure out if we should be using a proxy. Only if it's required, load | ||
// the https-proxy-agent module as it adds startup cost. | ||
function loadProxy() { | ||
const proxy = | ||
process.env.HTTPS_PROXY || | ||
|
@@ -53,10 +51,63 @@ function loadProxy() { | |
if (proxy) { | ||
HttpsProxyAgent = require('https-proxy-agent'); | ||
} | ||
return proxy; | ||
} | ||
loadProxy(); | ||
|
||
sofisl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
function matchingProxyStrings( | ||
envVarHTTPS: string | undefined, | ||
envVarHTTP: string | undefined, | ||
envVarhttps: string | undefined, | ||
envVarhttp: string | undefined, | ||
url: string | ||
) { | ||
const arrayOfEnvVariables = ( | ||
envVarHTTPS || | ||
envVarHTTP || | ||
envVarhttps || | ||
envVarhttp | ||
)?.split(','); | ||
|
||
let isMatch; | ||
if (arrayOfEnvVariables && arrayOfEnvVariables.length > 0) { | ||
const parsedURL = new URL(url); | ||
isMatch = arrayOfEnvVariables.find(url => { | ||
if (url.startsWith('*.') || url.startsWith('.')) { | ||
url = url.replace('*', ''); | ||
return parsedURL.hostname.endsWith(url); | ||
} else { | ||
return url === parsedURL.origin || url === parsedURL.hostname; | ||
} | ||
}); | ||
} | ||
return isMatch; | ||
} | ||
|
||
// Figure out if we should be using a proxy. Only if it's required, load | ||
// the https-proxy-agent module as it adds startup cost. | ||
function getProxy(url: string) { | ||
const shouldThisBeNoProxy = matchingProxyStrings( | ||
process.env.no_proxy, | ||
process.env.no_proxy, | ||
undefined, | ||
undefined, | ||
url | ||
); | ||
// If there is a match between the no_proxy env variables and the url, then do not proxy | ||
if (shouldThisBeNoProxy) { | ||
return undefined; | ||
// If there is not a match between the no_proxy env variables and the url, check to see if there should be a proxy | ||
} else { | ||
return matchingProxyStrings( | ||
process.env.HTTPS_PROXY, | ||
process.env.https_proxy, | ||
process.env.HTTP_PROXY, | ||
process.env.http_proxy, | ||
url | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should just return the proxy here, the logic is different for |
||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bcoe, I want to ask about the default behavior. If a URL comes in that is not listed under no_proxy or proxy, should it be proxied? Currently that's the way it is with the code as it stands now, but I feel like the default should be the other way around. In other words, we should maybe do some kind of checking to see if the URL equals any of the HTTPS_PROXY env variables. |
||
|
||
export class Gaxios { | ||
private agentCache = new Map< | ||
string, | ||
|
@@ -219,13 +270,14 @@ export class Gaxios { | |
} | ||
opts.method = opts.method || 'GET'; | ||
|
||
const proxy = loadProxy(); | ||
const proxy = getProxy(opts.url); | ||
if (proxy) { | ||
loadProxy(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't need this line anymore
sofisl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (this.agentCache.has(proxy)) { | ||
opts.agent = this.agentCache.get(proxy); | ||
opts.agent = this.agentCache.get(opts.url); | ||
} else { | ||
opts.agent = new HttpsProxyAgent(proxy); | ||
this.agentCache.set(proxy, opts.agent!); | ||
opts.agent = new HttpsProxyAgent(opts.url); | ||
this.agentCache.set(opts.url, opts.agent!); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -249,14 +249,181 @@ describe('🥁 configuration options', () => { | |
assert.deepStrictEqual(res.data, {}); | ||
}); | ||
|
||
it('should use an https proxy if asked nicely', async () => { | ||
sandbox.stub(process, 'env').value({https_proxy: 'https://fake.proxy'}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.ok(res.config.agent instanceof HttpsProxyAgent); | ||
describe('proxying', () => { | ||
it('should use an https proxy if asked nicely', async () => { | ||
const url = 'https://fake.proxy'; | ||
sandbox.stub(process, 'env').value({https_proxy: 'https://fake.proxy'}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.ok(res.config.agent instanceof HttpsProxyAgent); | ||
}); | ||
|
||
it('should not proxy when url matches no_proxy', async () => { | ||
const url = 'https://example.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'https://fake.proxy', | ||
no_proxy: 'https://example.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.strictEqual(res.config.agent, undefined); | ||
}); | ||
|
||
it('should not proxy if url does not match either env variable', async () => { | ||
const url = 'https://example2.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'https://fake.proxy', | ||
no_proxy: 'https://example.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.strictEqual(res.config.agent, undefined); | ||
}); | ||
|
||
it('should not proxy if no_proxy URL matches the origin or hostname of the URL', async () => { | ||
const url = 'https://example2.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'https://fake.proxy', | ||
no_proxy: 'example2.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.strictEqual(res.config.agent, undefined); | ||
}); | ||
|
||
it('should proxy if proxy URL matches the origin or hostname of the URL', async () => { | ||
const url = 'https://example2.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'example2.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.ok(res.config.agent instanceof HttpsProxyAgent); | ||
}); | ||
|
||
it('should not proxy if no_proxy env variable has asterisk', async () => { | ||
const url = 'https://domain.example.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'https://fake.proxy', | ||
no_proxy: '*.example.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.strictEqual(res.config.agent, undefined); | ||
}); | ||
|
||
it('should proxy if proxy env variable has asterisk', async () => { | ||
const url = 'https://domain.example.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: '*.example.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.ok(res.config.agent instanceof HttpsProxyAgent); | ||
}); | ||
|
||
it('should not proxy if no_proxy env variable starts with a dot', async () => { | ||
const url = 'https://domain.example.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'https://fake.proxy', | ||
no_proxy: '.example.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.strictEqual(res.config.agent, undefined); | ||
}); | ||
|
||
it('should proxy if proxy env variable starts with a dot', async () => { | ||
const url = 'https://domain.example.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: '.example.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.ok(res.config.agent instanceof HttpsProxyAgent); | ||
}); | ||
|
||
it('should allow comma-separated lists for no_proxy env variables', async () => { | ||
const url = 'https://api.google.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'https://fake.proxy', | ||
no_proxy: 'example.com,*.google.com,hello.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.strictEqual(res.config.agent, undefined); | ||
}); | ||
|
||
it('should allow comma-separated lists for proxy env variables', async () => { | ||
const url = 'https://api.google.com'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this test can be dropped. |
||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'example.com,*.google.com,hello.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.ok(res.config.agent instanceof HttpsProxyAgent); | ||
}); | ||
|
||
it('should let no_proxy take precedence, if url is in both proxy and no_proxy env variables', async () => { | ||
const url = 'https://example.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'example.com', | ||
no_proxy: 'example.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.strictEqual(res.config.agent, undefined); | ||
}); | ||
|
||
it('should proxy if url is an exact match of proxy, but not of no_proxy', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should also update this test to reflect the fact that there's no correlation between the URL and the It's |
||
const url = 'https://hello.example.com'; | ||
sandbox.stub(process, 'env').value({ | ||
https_proxy: 'https://hello.example.com', | ||
no_proxy: 'example.com', | ||
}); | ||
const body = {hello: '🌎'}; | ||
const scope = nock(url).get('/').reply(200, body); | ||
const res = await request({url}); | ||
scope.done(); | ||
assert.deepStrictEqual(res.data, body); | ||
assert.ok(res.config.agent instanceof HttpsProxyAgent); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I simply love the number of tests you wrote to cover this |
||
}); | ||
}); | ||
|
||
it('should load the proxy from the cache', async () => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So thinking back through this - I think I was front-loading this call so that the
require
call would always happen on app start, instead of potentially happening during a request and synchronously blocking the thread. By removing this, we could introduce a slower HTTP call here, and kind of degrade performance at a random point in app execution, no?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(sorry @bcoe I know you asked earlier, but it didn't make sense until I re-looked at the codes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JustinBeckwith, given that the function now depends on checking against a URL, wouldn't we have to wait for it to be called during a request?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just had a discussion with @sofisl about a potential refactor here, to get the best of both worlds.