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
3 changes: 2 additions & 1 deletion statsig-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
43 changes: 25 additions & 18 deletions statsig-node/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 44 additions & 6 deletions statsig-node/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -213,3 +250,4 @@ function _createErrorInstance(): Statsig {
dummyInstance.shutdown();
return dummyInstance;
}