From 667e212f6c8f5446fb0e23ab322d8595392bedc6 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 29 Jul 2026 14:25:16 +0530 Subject: [PATCH 1/2] fix: default to keep-alive connection agents in node environments httpAgent/httpsAgent defaulted to false, so every request opened a fresh TCP/TLS connection with zero reuse - relevant under Next.js prerendering concurrency, where redundant connection setup adds latency that pushes more requests toward the request timeout. Default to a keepAlive http.Agent/https.Agent instead, guarded behind a Node-only runtime check (typeof window === 'undefined', matching the existing isBrowser() convention in contentstack-typescript) since @contentstack/core has no browser field to redirect Node-only imports away from browser bundles the way axios does for itself. A fresh agent is created per httpClient() call rather than a shared module-level singleton - a deliberate tradeoff to avoid shared state across separate stack() instances in the same process. No behavior change for callers who already set their own httpAgent/ httpsAgent (including explicit false), and no behavior change at all in browser environments. DX-9991 --- src/lib/contentstack-core.ts | 6 ++++-- test/contentstack-core.node-agent.spec.ts | 22 ++++++++++++++++++++++ test/contentstack-core.spec.ts | 23 +++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 test/contentstack-core.node-agent.spec.ts diff --git a/src/lib/contentstack-core.ts b/src/lib/contentstack-core.ts index 69f7b3c..1c469ef 100644 --- a/src/lib/contentstack-core.ts +++ b/src/lib/contentstack-core.ts @@ -4,6 +4,8 @@ import axios, { AxiosRequestHeaders, getAdapter } from 'axios'; import { AxiosInstance, HttpClientParams } from './types'; import { ERROR_MESSAGES } from './error-messages'; +const isNodeEnvironment = typeof window === 'undefined'; + export function httpClient(options: HttpClientParams): AxiosInstance { const defaultConfig = { insecure: false, @@ -11,8 +13,8 @@ export function httpClient(options: HttpClientParams): AxiosInstance { headers: {} as AxiosRequestHeaders, basePath: '', proxy: false as const, - httpAgent: false, - httpsAgent: false, + httpAgent: isNodeEnvironment ? new (require('http').Agent)({ keepAlive: true }) : false, + httpsAgent: isNodeEnvironment ? new (require('https').Agent)({ keepAlive: true }) : false, timeout: 30000, logHandler: (level: string, data?: any) => { if (level === 'error') { diff --git a/test/contentstack-core.node-agent.spec.ts b/test/contentstack-core.node-agent.spec.ts new file mode 100644 index 0000000..508345b --- /dev/null +++ b/test/contentstack-core.node-agent.spec.ts @@ -0,0 +1,22 @@ +/** + * @jest-environment node + */ +import http from 'http'; +import https from 'https'; +import { httpClient } from '../src/lib/contentstack-core'; + +describe('httpClient default connection agents (Node environment)', () => { + it('should default httpAgent to a keepAlive http.Agent when not explicitly provided', () => { + const instance = httpClient({}); + + expect(instance.defaults.httpAgent).toBeInstanceOf(http.Agent); + expect((instance.defaults.httpAgent as any).keepAlive).toBe(true); + }); + + it('should default httpsAgent to a keepAlive https.Agent when not explicitly provided', () => { + const instance = httpClient({}); + + expect(instance.defaults.httpsAgent).toBeInstanceOf(https.Agent); + expect((instance.defaults.httpsAgent as any).keepAlive).toBe(true); + }); +}); diff --git a/test/contentstack-core.spec.ts b/test/contentstack-core.spec.ts index 0c4da51..abd0e7b 100644 --- a/test/contentstack-core.spec.ts +++ b/test/contentstack-core.spec.ts @@ -77,6 +77,29 @@ describe('contentstackCore', () => { }); }); + describe('connection agents', () => { + it.each(['httpAgent', 'httpsAgent'])( + 'should preserve an explicitly provided %s instead of defaulting it', + (agentOption) => { + const customAgent = { custom: true }; + const options = { [agentOption]: customAgent }; + + const instance = httpClient(options as any); + + expect((instance.defaults as any)[agentOption]).toEqual(customAgent); + } + ); + + it.each(['httpAgent', 'httpsAgent'])('should default %s to false in a browser-like environment', (agentOption) => { + // This spec file runs under jsdom (see jest.preset.js), so `window` is + // already defined here - matching a real browser, unlike the Node-only + // agent behavior covered in contentstack-core.node-agent.spec.ts. + const instance = httpClient({}); + + expect((instance.defaults as any)[agentOption]).toBe(false); + }); + }); + describe('config.headers', () => { it('should include apiKey in headers when provided', () => { const options = { From b62aa17ac4a215431c94fe5f2182d9ab072c00dc Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Wed, 29 Jul 2026 14:35:49 +0530 Subject: [PATCH 2/2] refactor: extract keep-alive agent creation into a helper function Removes duplication between the httpAgent and httpsAgent defaults. DX-9991 --- src/lib/contentstack-core.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib/contentstack-core.ts b/src/lib/contentstack-core.ts index 1c469ef..8a2aa2d 100644 --- a/src/lib/contentstack-core.ts +++ b/src/lib/contentstack-core.ts @@ -6,6 +6,15 @@ import { ERROR_MESSAGES } from './error-messages'; const isNodeEnvironment = typeof window === 'undefined'; +// Guarded require: keeps 'http'/'https' out of browser bundles, which have no browser field of their own to redirect this. +function createKeepAliveAgent(moduleName: 'http' | 'https') { + if (!isNodeEnvironment) { + return false as const; + } + + return new (require(moduleName).Agent)({ keepAlive: true }); +} + export function httpClient(options: HttpClientParams): AxiosInstance { const defaultConfig = { insecure: false, @@ -13,8 +22,8 @@ export function httpClient(options: HttpClientParams): AxiosInstance { headers: {} as AxiosRequestHeaders, basePath: '', proxy: false as const, - httpAgent: isNodeEnvironment ? new (require('http').Agent)({ keepAlive: true }) : false, - httpsAgent: isNodeEnvironment ? new (require('https').Agent)({ keepAlive: true }) : false, + httpAgent: createKeepAliveAgent('http'), + httpsAgent: createKeepAliveAgent('https'), timeout: 30000, logHandler: (level: string, data?: any) => { if (level === 'error') {