Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { client, ContentstackClient, ContentstackConfig } from '@contentstack/ma
import authHandler from './auth-handler';
import { Agent } from 'node:https';
import configHandler, { default as configStore } from './config-handler';
import { getProxyConfigForHost, resolveRequestHost, clearProxyEnv } from './proxy-helper';
import {
getProxyConfigForHost,
resolveRequestHost,
clearProxyEnv,
shouldBypassProxy,
} from './proxy-helper';
import dotenv from 'dotenv';

dotenv.config();
Expand All @@ -22,13 +27,13 @@ class ManagementSDKInitiator {
// NO_PROXY has priority over HTTP_PROXY/HTTPS_PROXY and config-set proxy
const proxyConfig = getProxyConfigForHost(host);

// When bypassing, clear proxy env immediately so SDK never see it (they may read at init or first request).
if (!proxyConfig) {
// When NO_PROXY matches, strip proxy env so SDK/axios cannot pick up HTTP_PROXY for this process.
if (host && shouldBypassProxy(host)) {
clearProxyEnv();
}

const option: ContentstackConfig = {
host: config.host,
host: config.host || host || undefined,
maxContentLength: config.maxContentLength || 100000000,
maxBodyLength: config.maxBodyLength || 1000000000,
maxRequests: 10,
Expand Down Expand Up @@ -118,7 +123,10 @@ class ManagementSDKInitiator {

if (proxyConfig) {
option.proxy = proxyConfig;
} else if (host && shouldBypassProxy(host)) {
option.proxy = false;
}
// When host is in NO_PROXY, do not add proxy to option at all
if (config.endpoint) {
option.endpoint = config.endpoint;
}
Expand Down
22 changes: 15 additions & 7 deletions packages/contentstack-utilities/src/http-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@ import { IHttpClient } from './client-interface';
import { HttpResponse } from './http-response';
import configStore from '../config-handler';
import authHandler from '../auth-handler';
import { hasProxy, getProxyUrl, getProxyConfig, getProxyConfigForHost } from '../proxy-helper';
import {
hasProxy,
getProxyUrl,
getProxyConfigForHost,
resolveRequestHost,
shouldBypassProxy,
} from '../proxy-helper';

/**
* Derive request host from baseURL or url for NO_PROXY checks.
*/
function getRequestHost(baseURL?: string, url?: string): string | undefined {
const toTry = [baseURL, url].filter(Boolean) as string[];
for (const candidateUrl of toTry) {
for (const u of toTry) {
try {
const parsed = new URL(candidateUrl.startsWith('http') ? candidateUrl : `https://${candidateUrl}`);
const parsed = new URL(u.startsWith('http') ? u : `https://${u}`);
return parsed.hostname || undefined;
} catch {
// Invalid URL; try next candidate (baseURL or url)
// ignore
}
}
return undefined;
Expand Down Expand Up @@ -427,12 +433,14 @@ export class HttpClient implements IHttpClient {
}
}

// Configure proxy if available. NO_PROXY has priority: hosts in NO_PROXY never use proxy.
// Configure proxy if available. NO_PROXY has priority; fall back to region CMA for host resolution.
if (!this.request.proxy) {
const host = getRequestHost(this.request.baseURL, url);
const proxyConfig = host ? getProxyConfigForHost(host) : getProxyConfig();
const host = getRequestHost(this.request.baseURL, url) || resolveRequestHost({});
const proxyConfig = getProxyConfigForHost(host);
if (proxyConfig) {
this.request.proxy = proxyConfig;
} else if (host && shouldBypassProxy(host)) {
this.request.proxy = false;
}
}

Expand Down
116 changes: 64 additions & 52 deletions packages/contentstack-utilities/src/proxy-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,44 +82,14 @@ export function shouldBypassProxy(host: string): boolean {
}

/**
* Get proxy configuration. Sources (in order): env (HTTP_PROXY/HTTPS_PROXY), then global config
* from `csdx config:set:proxy --host <host> --port <port> --protocol <protocol>`.
* Get proxy configuration. Priority order (per spec):
* 1. Global CLI config from `csdx config:set:proxy --host <host> --port <port> --protocol <protocol>`
* 2. Environment variables (HTTPS_PROXY or HTTP_PROXY)
* For per-request use, prefer getProxyConfigForHost(host) so NO_PROXY overrides both sources.
* @returns ProxyConfig object or undefined if no proxy is configured
*/
export function getProxyConfig(): ProxyConfig | undefined {
// Priority 1: Environment variables (HTTPS_PROXY or HTTP_PROXY)
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;

if (proxyUrl) {
try {
const url = new URL(proxyUrl);
const defaultPort = url.protocol === 'https:' ? 443 : 80;
const port = url.port ? Number.parseInt(url.port, 10) : defaultPort;

if (!Number.isNaN(port) && port >= 1 && port <= 65535) {
const protocol = url.protocol.replace(':', '') as 'http' | 'https';
const proxyConfig: ProxyConfig = {
protocol: protocol,
host: url.hostname,
port: port,
};

if (url.username || url.password) {
proxyConfig.auth = {
username: url.username,
password: url.password,
};
}

return proxyConfig;
}
} catch {
// Invalid URL, continue to check global config
}
}

// Priority 2: Global config (csdx config:set:proxy)
// Priority 1: Global config (csdx config:set:proxy)
const globalProxyConfig = configStore.get('proxy');
if (globalProxyConfig) {
if (typeof globalProxyConfig === 'object') {
Expand Down Expand Up @@ -151,11 +121,42 @@ export function getProxyConfig(): ProxyConfig | undefined {
return proxyConfig;
}
} catch {
// Invalid URL, return undefined
// Invalid URL, continue to check environment
}
}
}

// Priority 2: Environment variables (HTTPS_PROXY or HTTP_PROXY)
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;

if (proxyUrl) {
try {
const url = new URL(proxyUrl);
const defaultPort = url.protocol === 'https:' ? 443 : 80;
const port = url.port ? Number.parseInt(url.port, 10) : defaultPort;

if (!Number.isNaN(port) && port >= 1 && port <= 65535) {
const protocol = url.protocol.replace(':', '') as 'http' | 'https';
const proxyConfig: ProxyConfig = {
protocol: protocol,
host: url.hostname,
port: port,
};

if (url.username || url.password) {
proxyConfig.auth = {
username: url.username,
password: url.password,
};
}

return proxyConfig;
}
} catch {
// Invalid URL, return undefined
}
}

return undefined;
}

Expand All @@ -172,27 +173,38 @@ export function getProxyConfigForHost(host: string): ProxyConfig | undefined {
return getProxyConfig();
}

function regionCmaHostname(): string {
const cma = configStore.get('region')?.cma;
if (!cma || typeof cma !== 'string') {
return '';
}
if (cma.startsWith('http')) {
try {
const u = new URL(cma);
return u.hostname || cma;
} catch {
return cma;
}
}
return cma;
}

/**
* Resolve request host for proxy/NO_PROXY checks: config.host or default CMA from region.
* Use when the caller may omit host so NO_PROXY still applies (e.g. from region.cma).
* @param config - Object with optional host (e.g. API client config)
* @returns Host string (hostname or empty)
* Hostname for NO_PROXY / proxy. Prefer `region.cma` when set so callers that pass a
* default SDK host (e.g. bulk-entries -> api.contentstack.io) still match rules like
* `.csnonprod.com` against the real API host (e.g. dev11-api.csnonprod.com).
*/
export function resolveRequestHost(config: { host?: string }): string {
if (config.host) return config.host;
const cma = configStore.get('region')?.cma;
if (cma && typeof cma === 'string') {
if (cma.startsWith('http')) {
try {
const u = new URL(cma);
return u.hostname || cma;
} catch {
return cma;
}
}
return cma;
const fromRegion = regionCmaHostname();
if (fromRegion) {
return normalizeHost(fromRegion) || fromRegion;
}

const raw = config.host?.trim() || '';
if (!raw) {
return '';
}
return '';
return normalizeHost(raw) || raw;
}

/**
Expand Down
Loading