Skip to content

Commit 10a21b8

Browse files
sunil-lakshmanharshitha-cstk
authored andcommitted
Merge pull request #2252 from contentstack/enh/proxy-setup
Added proxy support
1 parent 5002713 commit 10a21b8

8 files changed

Lines changed: 362 additions & 4 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { cliux, configHandler, TableHeader, log, handleAndLogError } from '@contentstack/cli-utilities';
2+
import { BaseCommand } from '../../../base-command';
3+
4+
export default class ProxyGetCommand extends BaseCommand<typeof ProxyGetCommand> {
5+
static description = 'Get proxy configuration for CLI';
6+
7+
static examples = ['csdx config:get:proxy'];
8+
9+
async run() {
10+
try {
11+
log.debug('Starting proxy configuration retrieval', this.contextDetails);
12+
const globalProxyConfig = configHandler.get('proxy');
13+
14+
if (globalProxyConfig) {
15+
log.debug('Proxy configuration found in global config', this.contextDetails);
16+
let usernameValue = 'Not set';
17+
if (globalProxyConfig.auth?.username) {
18+
usernameValue = globalProxyConfig.auth.username;
19+
}
20+
21+
const proxyConfigList = [
22+
{
23+
Setting: 'Host',
24+
Value: globalProxyConfig.host || 'Not set',
25+
},
26+
{
27+
Setting: 'Port',
28+
Value: globalProxyConfig.port ? String(globalProxyConfig.port) : 'Not set',
29+
},
30+
{
31+
Setting: 'Protocol',
32+
Value: globalProxyConfig.protocol || 'Not set',
33+
},
34+
{
35+
Setting: 'Username',
36+
Value: usernameValue,
37+
},
38+
{
39+
Setting: 'Password',
40+
Value: globalProxyConfig.auth?.password ? '***' : 'Not set',
41+
},
42+
];
43+
44+
const headers: TableHeader[] = [{ value: 'Setting' }, { value: 'Value' }];
45+
46+
cliux.table(headers, proxyConfigList);
47+
log.info('Proxy configuration displayed successfully', this.contextDetails);
48+
} else {
49+
log.debug('No proxy configuration found in global config', this.contextDetails);
50+
}
51+
} catch (error) {
52+
handleAndLogError(error, { ...this.contextDetails, module: 'config-get-proxy' });
53+
}
54+
}
55+
}
56+
57+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { configHandler, log, handleAndLogError } from '@contentstack/cli-utilities';
2+
import { BaseCommand } from '../../../base-command';
3+
4+
export default class ProxyRemoveCommand extends BaseCommand<typeof ProxyRemoveCommand> {
5+
static description = 'Remove proxy configuration from global config';
6+
7+
static examples = ['csdx config:remove:proxy'];
8+
9+
async run() {
10+
try {
11+
log.debug('Starting proxy configuration removal', this.contextDetails);
12+
const currentProxy = configHandler.get('proxy');
13+
if (!currentProxy) {
14+
log.debug('No proxy configuration found in global config', this.contextDetails);
15+
return;
16+
}
17+
18+
log.debug('Removing proxy configuration from global config', this.contextDetails);
19+
configHandler.delete('proxy');
20+
log.success('Proxy configuration removed from global config successfully', this.contextDetails);
21+
} catch (error) {
22+
handleAndLogError(error, { ...this.contextDetails, module: 'config-remove-proxy' });
23+
}
24+
}
25+
}
26+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { flags, configHandler, FlagInput, log, handleAndLogError, cliux } from '@contentstack/cli-utilities';
2+
import { askProxyPassword } from '../../../utils/interactive';
3+
import { BaseCommand } from '../../../base-command';
4+
5+
export default class ProxySetCommand extends BaseCommand<typeof ProxySetCommand> {
6+
static description = 'Set proxy configuration for CLI';
7+
8+
static flags: FlagInput = {
9+
host: flags.string({
10+
description: 'Proxy host address',
11+
required: true,
12+
}),
13+
port: flags.string({
14+
description: 'Proxy port number',
15+
required: true,
16+
}),
17+
protocol: flags.string({
18+
description: 'Proxy protocol (http or https)',
19+
options: ['http', 'https'],
20+
default: 'http',
21+
required: true,
22+
}),
23+
username: flags.string({
24+
description: 'Proxy username (optional)',
25+
}),
26+
};
27+
28+
static examples = [
29+
'csdx config:set:proxy --host 127.0.0.1 --port 3128',
30+
'csdx config:set:proxy --host proxy.example.com --port 8080 --protocol https',
31+
'csdx config:set:proxy --host proxy.example.com --port 8080 --username user',
32+
];
33+
34+
async run() {
35+
try {
36+
log.debug('Starting proxy configuration setup', this.contextDetails);
37+
const { flags } = await this.parse(ProxySetCommand);
38+
39+
log.debug('Parsed proxy configuration flags', this.contextDetails);
40+
41+
// Validate host - must not be empty or whitespace-only
42+
if (!flags.host || flags.host.trim() === '') {
43+
log.error('Invalid host provided - host cannot be empty or whitespace-only', this.contextDetails);
44+
cliux.error('Invalid host address. Host cannot be empty or contain only whitespace.');
45+
return;
46+
}
47+
48+
const port = Number.parseInt(flags.port, 10);
49+
if (Number.isNaN(port) || port < 1 || port > 65535) {
50+
log.error('Invalid port number provided', this.contextDetails);
51+
cliux.error('Invalid port number. Port must be between 1 and 65535.');
52+
return;
53+
}
54+
55+
const proxyConfig: any = {
56+
protocol: flags.protocol || 'http',
57+
host: flags.host.trim(),
58+
port: port,
59+
};
60+
61+
if (flags.username) {
62+
log.debug('Username provided, prompting for password', this.contextDetails);
63+
// Prompt for password when username is provided
64+
const password = await askProxyPassword();
65+
proxyConfig.auth = {
66+
username: flags.username,
67+
password: password || '',
68+
};
69+
log.debug('Proxy authentication configured', this.contextDetails);
70+
}
71+
72+
log.debug('Saving proxy configuration to global config', this.contextDetails);
73+
configHandler.set('proxy', proxyConfig);
74+
75+
log.success('Proxy configuration set successfully', this.contextDetails);
76+
} catch (error) {
77+
handleAndLogError(error, { ...this.contextDetails, module: 'config-set-proxy' });
78+
}
79+
}
80+
}
81+

packages/contentstack-config/src/utils/interactive.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,14 @@ export async function askLogPath(): Promise<string> {
128128
]);
129129
return logPath;
130130
}
131+
132+
export async function askProxyPassword(): Promise<string> {
133+
return cliux.inquire<string>({
134+
type: 'input',
135+
message: 'Enter proxy password:',
136+
name: 'password',
137+
transformer: (password: string) => {
138+
return '*'.repeat(password.length);
139+
},
140+
});
141+
}

packages/contentstack-utilities/src/contentstack-management-sdk.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { client, ContentstackClient, ContentstackConfig } from '@contentstack/ma
22
import authHandler from './auth-handler';
33
import { Agent } from 'node:https';
44
import configHandler, { default as configStore } from './config-handler';
5+
import { getProxyConfig } from './proxy-helper';
6+
import dotenv from 'dotenv';
7+
8+
dotenv.config();
59

610
class ManagementSDKInitiator {
711
private analyticsInfo: string;
@@ -13,13 +17,16 @@ class ManagementSDKInitiator {
1317
}
1418

1519
async createAPIClient(config): Promise<ContentstackClient> {
20+
// Get proxy configuration with priority: Environment variables > Global config
21+
const proxyConfig = getProxyConfig();
22+
1623
const option: ContentstackConfig = {
1724
host: config.host,
1825
maxContentLength: config.maxContentLength || 100000000,
1926
maxBodyLength: config.maxBodyLength || 1000000000,
2027
maxRequests: 10,
2128
retryLimit: 3,
22-
timeout: 60000,
29+
timeout: proxyConfig ? 10000 : 60000, // 10s timeout with proxy, 60s without
2330
delayMs: config.delayMs,
2431
httpsAgent: new Agent({
2532
maxSockets: 100,
@@ -32,6 +39,31 @@ class ManagementSDKInitiator {
3239
retryDelay: Math.floor(Math.random() * (8000 - 3000 + 1) + 3000),
3340
logHandler: (level, data) => {},
3441
retryCondition: (error: any): boolean => {
42+
// Don't retry proxy connection errors - fail fast
43+
// Check if proxy is configured and this is a connection error
44+
if (proxyConfig) {
45+
const errorCode = error.code || error.errno || error.syscall;
46+
const errorMessage = error.message || String(error);
47+
48+
// Detect proxy connection failures - don't retry these
49+
if (
50+
errorCode === 'ECONNREFUSED' ||
51+
errorCode === 'ETIMEDOUT' ||
52+
errorCode === 'ENOTFOUND' ||
53+
errorCode === 'ERR_BAD_RESPONSE' ||
54+
errorMessage?.includes('ECONNREFUSED') ||
55+
errorMessage?.includes('connect ECONNREFUSED') ||
56+
errorMessage?.includes('ERR_BAD_RESPONSE') ||
57+
errorMessage?.includes('Bad response') ||
58+
errorMessage?.includes('proxy') ||
59+
(errorCode === 'ETIMEDOUT' && proxyConfig) // Timeout with proxy likely means proxy issue
60+
) {
61+
// Don't retry - return false to fail immediately
62+
// The error will be thrown and handled by error formatter
63+
return false;
64+
}
65+
}
66+
3567
// LINK ***REMOVED***vascript/blob/72fee8ad75ba7d1d5bab8489ebbbbbbaefb1c880/src/core/stack.js#L49
3668
if (error.response && error.response.status) {
3769
switch (error.response.status) {
@@ -45,6 +77,7 @@ class ManagementSDKInitiator {
4577
return false;
4678
}
4779
}
80+
return false;
4881
},
4982
retryDelayOptions: {
5083
base: 1000,
@@ -76,6 +109,9 @@ class ManagementSDKInitiator {
76109
},
77110
};
78111

112+
if (proxyConfig) {
113+
option.proxy = proxyConfig;
114+
}
79115
if (config.endpoint) {
80116
option.endpoint = config.endpoint;
81117
}

packages/contentstack-utilities/src/helpers.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { checkSync } from 'recheck';
22
import traverse from 'traverse';
33
import authHandler from './auth-handler';
44
import { ContentstackClient, HttpClient, cliux, configHandler } from '.';
5+
import { hasProxy, getProxyUrl } from './proxy-helper';
56

67
export const isAuthenticated = () => authHandler.isAuthenticated();
78
export const doesBranchExist = async (stack, branchName) => {
@@ -176,7 +177,12 @@ export const formatError = function (error: any) {
176177
}
177178

178179
// ENHANCED: Handle network errors with user-friendly messages
179-
if (['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ENETUNREACH'].includes(parsedError?.code)) {
180+
const networkErrorCodes = ['ECONNREFUSED', 'ENOTFOUND', 'ETIMEDOUT', 'ENETUNREACH', 'ERR_BAD_RESPONSE'];
181+
if (networkErrorCodes.includes(parsedError?.code) || parsedError?.message?.includes('ERR_BAD_RESPONSE')) {
182+
// Check if proxy is configured and connection failed - likely proxy issue
183+
if (hasProxy()) {
184+
return `Proxy error: Unable to connect to proxy server at ${getProxyUrl()}. Please verify your proxy configuration. Error: ${parsedError?.code || 'Unknown'}`;
185+
}
180186
return `Connection failed: Unable to reach the server. Please check your internet connection.`;
181187
}
182188

packages/contentstack-utilities/src/http-client/client.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IHttpClient } from './client-interface';
33
import { HttpResponse } from './http-response';
44
import configStore from '../config-handler';
55
import authHandler from '../auth-handler';
6+
import { hasProxy, getProxyUrl, getProxyConfig } from '../proxy-helper';
67

78
export type HttpClientOptions = {
89
disableEarlyAccessHeaders?: boolean;
@@ -358,7 +359,20 @@ export class HttpClient implements IHttpClient {
358359
async createAndSendRequest(method: HttpMethod, url: string): Promise<AxiosResponse> {
359360
let counter = 0;
360361
this.axiosInstance.interceptors.response.use(null, async (error) => {
361-
const { message, response } = error;
362+
const { message, response, code } = error;
363+
364+
// Don't retry proxy connection errors - fail fast
365+
const proxyErrorCodes = ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'ERR_BAD_RESPONSE'];
366+
const isProxyConfigured = this.request.proxy || hasProxy();
367+
368+
if (isProxyConfigured && (proxyErrorCodes.includes(code) || message?.includes('ERR_BAD_RESPONSE'))) {
369+
const proxyUrl = this.request.proxy && typeof this.request.proxy === 'object'
370+
? `${this.request.proxy.protocol}://${this.request.proxy.host}:${this.request.proxy.port}`
371+
: getProxyUrl();
372+
373+
return Promise.reject(new Error(`Proxy error: Unable to connect to proxy server at ${proxyUrl}. Please verify your proxy configuration.`));
374+
}
375+
362376
if (response?.data?.error_message?.includes('access token is invalid or expired')) {
363377
const token = await this.refreshToken();
364378
this.headers({ ...this.request.headers, authorization: token.authorization });
@@ -370,7 +384,7 @@ export class HttpClient implements IHttpClient {
370384
data: this.prepareRequestPayload(),
371385
});
372386
}
373-
// Retry while Network timeout or Network Error
387+
374388
if (
375389
!(message.includes('timeout') || message.includes('Network Error') || message.includes('getaddrinfo ENOTFOUND'))
376390
) {
@@ -397,6 +411,14 @@ export class HttpClient implements IHttpClient {
397411
}
398412
}
399413

414+
// Configure proxy if available (priority: request.proxy > getProxyConfig())
415+
if (!this.request.proxy) {
416+
const proxyConfig = getProxyConfig();
417+
if (proxyConfig) {
418+
this.request.proxy = proxyConfig;
419+
}
420+
}
421+
400422
return await this.axiosInstance({
401423
url,
402424
method,

0 commit comments

Comments
 (0)