Skip to content
Draft
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,8 +27,8 @@ 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 the SDK/axios cannot pick up HTTP_PROXY for this process.
if (host && shouldBypassProxy(host)) {
clearProxyEnv();
}

Expand Down Expand Up @@ -118,6 +123,8 @@ class ManagementSDKInitiator {

if (proxyConfig) {
option.proxy = proxyConfig;
} else if (host && shouldBypassProxy(host)) {
option.proxy = false;
}
if (config.endpoint) {
option.endpoint = config.endpoint;
Expand Down
120 changes: 70 additions & 50 deletions packages/contentstack-utilities/src/http-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ 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';

type AxiosRequestConfigWithRetry = AxiosRequestConfig & { __httpClientRetryCount?: number };

/**
* Derive request host from baseURL or url for NO_PROXY checks.
Expand Down Expand Up @@ -52,6 +60,62 @@ export class HttpClient implements IHttpClient {

// Sets payload format as json by default
this.asJson();
this.attachResponseInterceptor();
}

/** Single interceptor per instance — avoids stacking handlers on every request (major perf win). */
private attachResponseInterceptor(): void {
this.axiosInstance.interceptors.response.use(null, async (error) => {
const cfg = error.config as AxiosRequestConfigWithRetry | undefined;
if (!cfg) {
return Promise.reject(error);
}

const { message, response, code } = error;
const proxyFromCfg = cfg.proxy;
const isProxyConfigured = !!proxyFromCfg || hasProxy();

const proxyErrorCodes = ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'ERR_BAD_RESPONSE'];
if (isProxyConfigured && (proxyErrorCodes.includes(code) || message?.includes('ERR_BAD_RESPONSE'))) {
const p = proxyFromCfg as { protocol?: string; host?: string; port?: number } | undefined;
const proxyUrl =
p && typeof p === 'object' && p.host
? `${p.protocol ?? 'http'}://${p.host}:${p.port ?? 80}`
: getProxyUrl();

return Promise.reject(
new Error(
`Proxy error: Unable to connect to proxy server at ${proxyUrl}. Please verify your proxy configuration.`,
),
);
}

if (response?.data?.error_message?.includes('access token is invalid or expired')) {
const token = await this.refreshToken();
this.headers({ ...this.request.headers, authorization: token.authorization });
return this.axiosInstance({
...cfg,
headers: { ...cfg.headers, authorization: token.authorization },
});
}

if (
!(
message?.includes('timeout') ||
message?.includes('Network Error') ||
message?.includes('getaddrinfo ENOTFOUND')
)
) {
return Promise.reject(error);
}

const retries = cfg.__httpClientRetryCount ?? 0;
if (retries < 1) {
cfg.__httpClientRetryCount = retries + 1;
return this.axiosInstance(cfg);
}
return Promise.reject(error);
});
}

/**
Expand Down Expand Up @@ -373,52 +437,6 @@ export class HttpClient implements IHttpClient {
* @returns {Request}
*/
async createAndSendRequest(method: HttpMethod, url: string): Promise<AxiosResponse> {
let counter = 0;
this.axiosInstance.interceptors.response.use(null, async (error) => {
const { message, response, code } = error;

// Don't retry proxy connection errors - fail fast
const proxyErrorCodes = ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND', 'ERR_BAD_RESPONSE'];
const isProxyConfigured = this.request.proxy || hasProxy();

if (isProxyConfigured && (proxyErrorCodes.includes(code) || message?.includes('ERR_BAD_RESPONSE'))) {
const proxyUrl = this.request.proxy && typeof this.request.proxy === 'object'
? `${this.request.proxy.protocol}://${this.request.proxy.host}:${this.request.proxy.port}`
: getProxyUrl();

return Promise.reject(new Error(`Proxy error: Unable to connect to proxy server at ${proxyUrl}. Please verify your proxy configuration.`));
}

if (response?.data?.error_message?.includes('access token is invalid or expired')) {
const token = await this.refreshToken();
this.headers({ ...this.request.headers, authorization: token.authorization });
return await this.axiosInstance({
url,
method,
withCredentials: true,
...this.request,
data: this.prepareRequestPayload(),
});
}

if (
!(message.includes('timeout') || message.includes('Network Error') || message.includes('getaddrinfo ENOTFOUND'))
) {
return Promise.reject(error);
}
if (counter < 1) {
counter++;
return await this.axiosInstance({
url,
method,
withCredentials: true,
...this.request,
data: this.prepareRequestPayload(),
});
}
return Promise.reject(error);
});

if (!this.disableEarlyAccessHeaders) {
// Add early access header by default
const earlyAccessHeaders = configStore.get(`earlyAccessHeaders`);
Expand All @@ -427,12 +445,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
Loading
Loading