feat: add upstream proxy configuration for outbound requests#1458
feat: add upstream proxy configuration for outbound requests#1458Andreybest wants to merge 5 commits intofinos:mainfrom
Conversation
✅ Deploy Preview for endearing-brigadeiros-63f9d0 canceled.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1458 +/- ##
=======================================
Coverage 89.66% 89.67%
=======================================
Files 68 68
Lines 4869 4947 +78
Branches 888 918 +30
=======================================
+ Hits 4366 4436 +70
- Misses 485 493 +8
Partials 18 18 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
jescalada
left a comment
There was a problem hiding this comment.
@Andreybest Thanks for the contribution! 🚀
I left some comments for code refactoring. It'd be very helpful if you could upload images or a video demonstrating that this feature works as intended.
@coopernetes Wondering if this implements your original requirements (#759) and works on your end 🤔
| } | ||
| } | ||
| }, | ||
| "upstreamProxy": { |
There was a problem hiding this comment.
Sample values or just empty entries should be added to proxy.config.json so it's easier to see that the upstreamProxy is configurable and how to configure it:
"upstreamConfig": {
"enabled": true,
"url": "",
"noProxy": []
}
src/proxy/routes/index.ts
Outdated
| headers: OutgoingHttpHeaders; | ||
| }, | ||
| ) => { | ||
| const upstreamProxyConfig = getUpstreamProxyConfig(); |
There was a problem hiding this comment.
Refactored for simplicity:
| const upstreamProxyConfig = getUpstreamProxyConfig(); | |
| const { enabled, url, noProxy } = getUpstreamProxyConfig(); |
src/proxy/routes/index.ts
Outdated
| const configuredUrl = upstreamProxyConfig.url; | ||
| const envUrl = getEnvProxyUrl(); | ||
|
|
||
| const proxyUrl = configuredUrl || envUrl; |
There was a problem hiding this comment.
Short-circuiting so we don't execute getEnvProxyUrl() all the time
| const proxyUrl = configuredUrl || envUrl; | |
| const proxyUrl = url || getEnvProxyUrl(); |
(must also remove const envUrl = getEnvProxyUrl(); from above)
src/proxy/routes/index.ts
Outdated
| ) => { | ||
| const upstreamProxyConfig = getUpstreamProxyConfig(); | ||
|
|
||
| const configuredUrl = upstreamProxyConfig.url; |
There was a problem hiding this comment.
| const configuredUrl = upstreamProxyConfig.url; |
src/proxy/routes/index.ts
Outdated
| const upstreamProxyConfig = getUpstreamProxyConfig(); | ||
|
|
||
| const configuredUrl = upstreamProxyConfig.url; | ||
| const envUrl = getEnvProxyUrl(); |
There was a problem hiding this comment.
| const envUrl = getEnvProxyUrl(); |
src/proxy/routes/index.ts
Outdated
|
|
||
| const proxyUrl = configuredUrl || envUrl; | ||
|
|
||
| // If nothing is configured, do not use a proxy |
There was a problem hiding this comment.
Code here is self-descriptive 🙂
| // If nothing is configured, do not use a proxy |
src/proxy/routes/index.ts
Outdated
| return undefined; | ||
| } | ||
|
|
||
| // If config explicitly disabled the proxy, do not use it |
There was a problem hiding this comment.
Same as above
| // If config explicitly disabled the proxy, do not use it |
src/proxy/routes/index.ts
Outdated
|
|
||
| const host: string | null | undefined = proxyReqOpts.host || proxyReqOpts.hostname; | ||
|
|
||
| const configNoProxy = upstreamProxyConfig.noProxy ? upstreamProxyConfig.noProxy : []; |
There was a problem hiding this comment.
Not sure what was the intention here 🤔
src/proxy/routes/index.ts
Outdated
|
|
||
| const configNoProxy = upstreamProxyConfig.noProxy ? upstreamProxyConfig.noProxy : []; | ||
| const envNoProxy = getEnvNoProxyList(); | ||
| const combinedNoProxy = [...configNoProxy, ...envNoProxy]; |
There was a problem hiding this comment.
Do we want to combine the configuration + env values, or do we want to default to the env values if the noProxy from the config is missing despite upstreamProxy being enabled?
There was a problem hiding this comment.
If we want to combine, this could be done in a single line like such:
const combinedNoProxy = [...noProxy || [], ...getEnvNoProxyList()]If we want to use the env values as fallbacks, we can further simplify by stuffing everything in the if-statement below:
if (hostMatchesNoProxy(host, noProxy || getEnvNoProxyList())) {
return undefined;
}There was a problem hiding this comment.
I think we need to combine, selected first approach :)
There was a problem hiding this comment.
Let's aim for >90% coverage 😃
There was a problem hiding this comment.
89.87%, hopefully this passes :)
dcoric
left a comment
There was a problem hiding this comment.
Thanks for tackling #759. I've left a few comments focused on correctness and robustness, especially around NO_PROXY pattern handling which is important to get right for the target use case. The main items are the missing */leading-dot NO_PROXY support and proxy URL validation.
| .filter((entry) => entry.length > 0); | ||
| }; | ||
|
|
||
| const hostMatchesNoProxy = (host: string | null | undefined, noProxyList: string[]): boolean => { |
There was a problem hiding this comment.
hostMatchesNoProxy does not handle two standard NO_PROXY conventions that are universally used in corporate environments - exactly the target audience for this feature:
- Wildcard
*- means "bypass proxy for all hosts." Currently*would fall through to the.endsWithcheck and never match. - Leading-dot patterns like
.example.com- the current.endsWith(`.${trimmed}`)logic would look for..example.com, which never matches anything.
These conventions are supported by curl, wget, Go stdlib, Python requests, and Node's global-agent. Since this feature targets corporate proxy environments, both patterns will appear in real NO_PROXY values.
Suggested fix:
const trimmed = pattern.trim().replace(/^\./, ''); // strip leading dot
if (trimmed === '*') return true; // wildcard - bypass all
src/proxy/routes/index.ts
Outdated
| return undefined; | ||
| } | ||
|
|
||
| return new HttpsProxyAgent(proxyUrl); |
There was a problem hiding this comment.
The proxyUrl (from config or env vars) is passed directly to new HttpsProxyAgent(proxyUrl) without validation. If the URL is malformed (typo, missing protocol, empty string after trimming), this throws a cryptic Invalid URL error that surfaces through express-http-proxy's error handler with no indication that the upstream proxy config is the problem.
Consider wrapping this in a try-catch or validating first:
try {
new URL(proxyUrl); // validate before constructing agent
} catch {
throw new Error(`Invalid upstream proxy URL: check your upstreamProxy.url config or HTTPS_PROXY env var`);
}
return new HttpsProxyAgent(proxyUrl);| Configures routing of outbound requests from the GitProxy server to upstream Git hosts (e.g. GitHub, GitLab) via an HTTP(S) proxy. Use this when the server runs in an environment where direct Internet access is not allowed and all traffic must go through a corporate web proxy ("proxying the proxy"). | ||
|
|
||
| - **`enabled`** (boolean): When `true`, outbound connections to upstream Git hosts use the configured proxy. When `false`, the proxy is not used even if `url` or environment variables are set. | ||
| - **`url`** (string): The HTTP(S) proxy URL (e.g. `http://proxy.corp.local:8080` or `http://user:pass@proxy.corp.local:8080`). If omitted, GitProxy falls back to the `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY` or `http_proxy` environment variables (first defined wins). |
There was a problem hiding this comment.
The docs show http://user:pass@proxy.corp.local:8080 as a valid proxy URL format, which is correct - but the code has no redaction layer for this value. The proxyUrl containing credentials flows through buildUpstreamProxyAgent and into the request options. If express-http-proxy debug logging is enabled, or if logConfiguration prints the full config at startup, credentials will appear in plaintext in logs.
At minimum, consider:
- Adding a warning comment in the code that
proxyUrlmay contain credentials and must not be logged - If any logging is added in the future around proxy setup, use a helper that redacts the userinfo portion of the URL (everything between
://and@)
There was a problem hiding this comment.
Added warnings in comments. Also added function for reduction if it will be used in future
| }; | ||
|
|
||
| const proxyReqOptDecorator: ProxyOptions['proxyReqOptDecorator'] = (proxyReqOpts, _srcReq) => { | ||
| const agent = buildUpstreamProxyAgent(proxyReqOpts); |
There was a problem hiding this comment.
buildUpstreamProxyAgent is called from proxyReqOptDecorator on every proxied request. Each invocation creates a new HttpsProxyAgent(proxyUrl) and also calls getUpstreamProxyConfig() which runs loadFullConfiguration(). For high-throughput operations like large clones with many pack requests, this creates unnecessary object allocation and GC pressure.
Consider caching the agent instance and only recreating it when the proxy URL or configuration changes - for example, using a simple memoization keyed on proxyUrl.
There was a problem hiding this comment.
Considering that loadFullConfiguration() returns cached object:
function loadFullConfiguration(): GitProxyConfig {
if (_currentConfig) {
return _currentConfig;
}I don't think we don't have to worry about it, for new HttpsProxyAgent(proxyUrl) - cache is implemented
|
Thanks for the reviews @jescalada and @dcoric! |
|
@jescalada Screen.Recording.2026-03-25.at.21.35.35_no_audio.mov |
Co-authored-by: Thomas Cooper <57812123+coopernetes@users.noreply.github.com> Signed-off-by: Andrew <andrey255@live.com>
Resolves #759.
Adds upstream proxy support. Uses values from both configuration file in
upstreamProxyfields and environment variables (HTTPS_PROXY/HTTP_PROXY,NO_PROXY)