From 872d9c6ab5fa9565130a6ccd817279e05e78f7bb Mon Sep 17 00:00:00 2001 From: Andrew Holbrook Date: Sat, 20 Dec 2025 08:23:35 -0800 Subject: [PATCH] Fix EPIPE errors with proper HTTP agent connection pooling and timeouts - Replace https-proxy-agent with agentkeepalive and hpagent - Add proper timeout configuration (timeout: 30000, freeSocketTimeout: 15000) - Implement shared HTTP/HTTPS agents with keep-alive settings - Enhance proxy support with both HTTP and HTTPS agents - Maintain backward compatibility --- statsig-node/package.json | 3 ++- statsig-node/pnpm-lock.yaml | 43 +++++++++++++++++------------- statsig-node/src/lib/index.ts | 50 ++++++++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 25 deletions(-) diff --git a/statsig-node/package.json b/statsig-node/package.json index 9e2faaa23..33efc9b33 100644 --- a/statsig-node/package.json +++ b/statsig-node/package.json @@ -12,7 +12,8 @@ "repository": "https://github.com/statsig-io/statsig-server-core.git", "dependencies": { "@octokit/core": "^6", - "https-proxy-agent": "^7.0.6", + "agentkeepalive": "^4.5.0", + "hpagent": "^1.2.0", "node-fetch": "2.7.0" }, "devDependencies": { diff --git a/statsig-node/pnpm-lock.yaml b/statsig-node/pnpm-lock.yaml index a0cca7dd2..8e2a5e7a2 100644 --- a/statsig-node/pnpm-lock.yaml +++ b/statsig-node/pnpm-lock.yaml @@ -11,9 +11,12 @@ importers: '@octokit/core': specifier: ^6 version: 6.1.6 - https-proxy-agent: - specifier: ^7.0.6 - version: 7.0.6 + agentkeepalive: + specifier: ^4.5.0 + version: 4.6.0 + hpagent: + specifier: ^1.2.0 + version: 1.2.0 node-fetch: specifier: 2.7.0 version: 2.7.0 @@ -989,9 +992,9 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} @@ -1450,6 +1453,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1457,14 +1464,13 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -3276,7 +3282,9 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - agent-base@7.1.4: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 ansi-escapes@4.3.2: dependencies: @@ -3778,6 +3786,8 @@ snapshots: dependencies: function-bind: 1.1.2 + hpagent@1.2.0: {} + html-escaper@2.0.2: {} http-errors@2.0.0: @@ -3788,15 +3798,12 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - human-signals@2.1.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 diff --git a/statsig-node/src/lib/index.ts b/statsig-node/src/lib/index.ts index 21aef72e4..8bcbbe6b4 100644 --- a/statsig-node/src/lib/index.ts +++ b/statsig-node/src/lib/index.ts @@ -1,4 +1,5 @@ -import { HttpsProxyAgent } from 'https-proxy-agent'; +import HttpAgent, { HttpsAgent } from 'agentkeepalive'; +import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import nodeFetch from 'node-fetch'; import { @@ -34,7 +35,29 @@ ParameterStore.prototype[inspectSym] = function () { return this.toJSON(); }; -function createProxyAgent(options?: StatsigOptions) { +// Create shared HTTP agents with keep-alive and proper timeout settings +// agentkeepalive provides freeSocketTimeout support for all Node versions +// and handles connection pooling more robustly than built-in agents +const httpAgent = new HttpAgent({ + // Defaults: keepAlive=true, freeSocketTimeout=4000ms, timeout=8000ms + // Bump timeout to match SDK's default request timeout + timeout: 30000, +}); + +const httpsAgent = new HttpsAgent({ + timeout: 30000, +}); + +// Agent options with keepAlive settings to prevent EPIPE errors +const agentOptions = { + keepAlive: true, + keepAliveMsecs: 1000, + timeout: 30000, + freeSocketTimeout: 15000, + scheduling: 'fifo' as const, +}; + +function createProxyAgents(options?: StatsigOptions) { const proxy = options?.proxyConfig; if (proxy?.proxyHost && proxy?.proxyProtocol) { const protocol = proxy.proxyProtocol; @@ -44,14 +67,28 @@ function createProxyAgent(options?: StatsigOptions) { const proxyUrl = `${protocol}://${auth}${host}${port}`; if (protocol === 'http' || protocol === 'https') { - return new HttpsProxyAgent(proxyUrl); + // hpagent supports all standard agent options including keepAlive/freeSocketTimeout + return { + http: new HttpProxyAgent({ proxy: proxyUrl, ...agentOptions }), + https: new HttpsProxyAgent({ proxy: proxyUrl, ...agentOptions }), + }; } } - return undefined; // node-fetch agent parameter takes in undefined type instead of null + return undefined; +} + +function getAgent( + url: string, + proxyAgents?: { http: HttpProxyAgent; https: HttpsProxyAgent }, +) { + if (proxyAgents) { + return url.startsWith('https') ? proxyAgents.https : proxyAgents.http; + } + return url.startsWith('https') ? httpsAgent : httpAgent; } function createFetchFunc(options?: StatsigOptions) { - const proxyAgent = createProxyAgent(options); + const proxyAgents = createProxyAgents(options); return async ( method: string, @@ -67,7 +104,7 @@ function createFetchFunc(options?: StatsigOptions) { 'Accept-Encoding': 'gzip, deflate, br', }, body: body ? Buffer.from(body) : undefined, - agent: proxyAgent, + agent: getAgent(url, proxyAgents), }); const data = await res.arrayBuffer(); @@ -213,3 +250,4 @@ function _createErrorInstance(): Statsig { dummyInstance.shutdown(); return dummyInstance; } +