Skip to content

Commit 7d2be0f

Browse files
authored
feat: Replace agentkeepalive with native Node.js HTTP agents for proxy support (#788)
Hello 👋 Here is a PR proposition to be able to use `apify` CLI in a sandboxed environment (in my case, Claude code in the browser). At the moment, we can't do `apify login` or `apify info` in such environment, because the **apify-client** doesn't handle the proxy configuration properly. The commands are stuck. Digging into it, I found out that the `agentkeepalive` module doesn't support proxy (see node-modules/agentkeepalive#22 (comment) for instance). I first attempted to use [proxy-agents](https://github.com/TooTallNate/proxy-agents), but it turns out the built-in agents can support both keepalive and proxies out of the box. So this PR removes `agentkeepalive` while maintaining the same keep-alive functionality and adding proxy support capability. ### Test To test it locally, I created a transparent proxy (we don't intercept requests to avoid self-certificate issues): ```sh mitmdump --listen-port 8000 --ignore-hosts '.*:*' --set dns_log=true ``` And in another terminal, I tested the Apify client with : ```sh export HTTPS_PROXY=http://localhost:8000 export APIFY_TOKEN=[REPLACE_WITH_VALID_TOKEN] node get-user-info.js # See code below ``` ```js #!/usr/bin/env node const { ApifyClient } = require('./dist/index.js'); async function main() { // Get token from environment variable const token = process.env.APIFY_TOKEN; if (!token) { console.error('Error: APIFY_TOKEN environment variable is not set'); process.exit(1); } try { const client = new ApifyClient({ token }); console.log('Fetching user information...\n'); const userInfo = await client.user().get(); console.log('User Information:'); console.log(JSON.stringify(userInfo, null, 2)); } catch (error) { console.error('Error fetching user information:'); console.error(error.message); if (error.statusCode) { console.error(`Status Code: ${error.statusCode}`); } process.exit(1); } } main(); ```
1 parent 5860629 commit 7d2be0f

File tree

4 files changed

+36
-34
lines changed

4 files changed

+36
-34
lines changed

package-lock.json

Lines changed: 1 addition & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
"@apify/log": "^2.2.6",
6767
"@apify/utilities": "^2.23.2",
6868
"@crawlee/types": "^3.3.0",
69-
"agentkeepalive": "^4.2.1",
7069
"ansi-colors": "^4.1.1",
7170
"async-retry": "^1.3.3",
7271
"axios": "^1.6.7",

rsbuild.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export default defineConfig({
3131
externals: {
3232
'node:util': 'node:util',
3333
'node:zlib': 'node:zlib',
34+
'node:http': 'node:http',
35+
'node:https': 'node:https',
3436
crypto: 'node:crypto',
3537
},
3638
},

src/http_client.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import http from 'node:http';
2+
import https from 'node:https';
13
import os from 'node:os';
24

3-
import KeepAliveAgent from 'agentkeepalive';
45
import type { RetryFunction } from 'async-retry';
56
import retry from 'async-retry';
67
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
@@ -32,9 +33,9 @@ export class HttpClient {
3233

3334
timeoutMillis: number;
3435

35-
httpAgent?: KeepAliveAgent;
36+
httpAgent?: http.Agent;
3637

37-
httpsAgent?: KeepAliveAgent.HttpsAgent;
38+
httpsAgent?: https.Agent;
3839

3940
axios: AxiosInstance;
4041

@@ -53,16 +54,37 @@ export class HttpClient {
5354

5455
if (isNode()) {
5556
// We want to keep sockets alive for better performance.
56-
// It's important to set the user's timeout also here and not only
57-
// on the axios instance, because even though this timeout
58-
// is for inactive sockets, sometimes the platform would take
59-
// long to process requests and the socket would time-out
60-
// while waiting for the response.
61-
const agentOpts = {
57+
// Enhanced agent configuration based on agentkeepalive best practices:
58+
// - Nagle's algorithm disabled for lower latency
59+
// - Free socket timeout to prevent socket leaks
60+
// - LIFO scheduling to reuse recent sockets
61+
// - Socket TTL for connection freshness
62+
const agentOptions: http.AgentOptions & { scheduling?: 'lifo' | 'fifo' } = {
63+
keepAlive: true,
64+
// Timeout for inactive sockets
65+
// Prevents socket leaks from idle connections
6266
timeout: this.timeoutMillis,
67+
// Keep alive timeout for free sockets (15 seconds)
68+
// Node.js will close unused sockets after this period
69+
keepAliveMsecs: 15_000,
70+
// Maximum number of sockets per host
71+
maxSockets: 256,
72+
maxFreeSockets: 256,
73+
// LIFO scheduling - reuse most recently used sockets for better performance
74+
scheduling: 'lifo',
6375
};
64-
this.httpAgent = new KeepAliveAgent(agentOpts);
65-
this.httpsAgent = new KeepAliveAgent.HttpsAgent(agentOpts);
76+
77+
this.httpAgent = new http.Agent(agentOptions);
78+
this.httpsAgent = new https.Agent(agentOptions);
79+
80+
// Disable Nagle's algorithm for lower latency
81+
// This sends data immediately instead of buffering small packets
82+
const setNoDelay = (socket: any) => {
83+
socket.setNoDelay(true);
84+
};
85+
86+
this.httpAgent.on('socket', setNoDelay);
87+
this.httpsAgent.on('socket', setNoDelay);
6688
}
6789

6890
this.axios = axios.create({

0 commit comments

Comments
 (0)