Node.js
undici、axios、got 和原生 fetch 的代理接入,含连接池控制与轮换。
Node.js 的内置 fetch 和 http 模块不读 HTTP_PROXY 环境变量,也不能直接配代理。必须显式用一个 agent 或 dispatcher。
undici(推荐)
Node 18+ 内置了 undici 作为 fetch 的实现,它的 ProxyAgent 是目前最干净的方案。
npm install undici
import { ProxyAgent, request } from 'undici';
const PROXY = 'http://USERNAME-country-US-session-job1-time-30:PASSWORD@GATEWAY_HOST:58971';
const dispatcher = new ProxyAgent(PROXY);
const { statusCode, body } = await request('https://api.ipify.org?format=json', {
dispatcher,
});
console.log(statusCode, await body.json());
await dispatcher.close();
配合全局 fetch
import { ProxyAgent, setGlobalDispatcher } from 'undici';
setGlobalDispatcher(
new ProxyAgent('http://USERNAME-country-US:PASSWORD@GATEWAY_HOST:58971'),
);
// 之后所有的 fetch 都走代理
const response = await fetch('https://api.ipify.org');
console.log(await response.text());
轮换 IP
不带 session,并且每次用新的 dispatcher:
import { ProxyAgent, request } from 'undici';
const PROXY = 'http://USERNAME-country-US:PASSWORD@GATEWAY_HOST:58971';
for (let i = 0; i < 5; i += 1) {
const dispatcher = new ProxyAgent(PROXY);
const { body } = await request('https://api.ipify.org', { dispatcher });
console.log(await body.text());
await dispatcher.close();
}
并发 + 每任务独立会话
import { ProxyAgent, request } from 'undici';
const GATEWAY = 'GATEWAY_HOST:58971';
const USER = 'USERNAME';
const PASSWORD = 'PASSWORD';
function proxyFor(sessionId, country = 'US', minutes = 30) {
const user = `${USER}-country-${country}-session-${sessionId}-time-${minutes}`;
return `http://${user}:${PASSWORD}@${GATEWAY}`;
}
async function fetchWith(sessionId) {
const dispatcher = new ProxyAgent(proxyFor(sessionId));
try {
const { body } = await request('https://api.ipify.org', { dispatcher });
return await body.text();
} finally {
await dispatcher.close();
}
}
const results = await Promise.all(
// session 值只允许 [A-Za-z0-9.-],用点号不用下划线
Array.from({ length: 8 }, (_, i) => fetchWith(`task.${i}`)),
);
console.log(results);
控制连接池
import { ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent({
uri: 'http://USERNAME-country-US:PASSWORD@GATEWAY_HOST:58971',
connections: 16,
// 网关空闲超时是 5 分钟,客户端设得更短能避免用到已被关闭的连接
keepAliveTimeout: 60_000,
keepAliveMaxTimeout: 240_000,
});
axios
axios 的 proxy 配置项对 HTTPS 请求处理得不好,实践中应该用 httpsAgent:
npm install axios https-proxy-agent
import axios from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
const PROXY = 'http://USERNAME-country-US-session-job1-time-30:PASSWORD@GATEWAY_HOST:58971';
const agent = new HttpsProxyAgent(PROXY);
const client = axios.create({
httpAgent: agent,
httpsAgent: agent,
// 必须关掉,否则 axios 会尝试自己处理代理并和 agent 冲突
proxy: false,
timeout: 30_000,
});
const { data } = await client.get('https://api.ipify.org?format=json');
console.log(data);
got
npm install got hpagent
import got from 'got';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
const PROXY = 'http://USERNAME-country-US:PASSWORD@GATEWAY_HOST:58971';
const client = got.extend({
agent: {
http: new HttpProxyAgent({ proxy: PROXY, keepAlive: true }),
https: new HttpsProxyAgent({ proxy: PROXY, keepAlive: true }),
},
timeout: { request: 30_000 },
retry: {
limit: 3,
// 先区分代理握手错误和目标响应;仅对可安全重放的请求退避重试
statusCodes: [429, 500, 502, 503, 504],
},
});
console.log(await client('https://api.ipify.org').text());
SOCKS5
npm install socks-proxy-agent
import { SocksProxyAgent } from 'socks-proxy-agent';
import { request } from 'undici';
// socks5h 让代理解析域名
const agent = new SocksProxyAgent(
'socks5h://USERNAME-country-US:PASSWORD@GATEWAY_HOST:58971',
);
// 配合 node:https
import https from 'node:https';
https.get('https://api.ipify.org', { agent }, (response) => {
response.setEncoding('utf8');
response.on('data', (chunk) => process.stdout.write(chunk));
});
原生 http 模块 + CONNECT
需要完全控制协议交互时(比如自己实现指纹)可以手工发 CONNECT:
import http from 'node:http';
import tls from 'node:tls';
const USER = 'USERNAME-country-US';
const PASSWORD = 'PASSWORD';
const auth = Buffer.from(`${USER}:${PASSWORD}`).toString('base64');
const req = http.request({
host: 'GATEWAY_HOST',
port: 58971,
method: 'CONNECT',
path: 'api.ipify.org:443',
headers: { 'Proxy-Authorization': `Basic ${auth}` },
});
req.on('connect', (res, socket) => {
if (res.statusCode !== 200) {
console.error('隧道建立失败:', res.statusCode);
socket.destroy();
return;
}
const tlsSocket = tls.connect({ socket, servername: 'api.ipify.org' }, () => {
tlsSocket.write('GET / HTTP/1.1\r\nHost: api.ipify.org\r\nConnection: close\r\n\r\n');
});
tlsSocket.pipe(process.stdout);
});
req.end();
从提取接口拿代理池
const API = 'https://api.example.com/api/v1/proxy/extract';
async function fetchProxyPool(apiKey, count = 10) {
const params = new URLSearchParams({
apikey: apiKey,
num: String(count),
type: 'json',
country: 'US',
session: 'sticky',
time: '10',
});
const response = await fetch(`${API}?${params}`);
if (!response.ok) {
throw new Error(`提取失败: ${response.status} ${await response.text()}`);
}
const { data } = await response.json();
// 免密端口,不需要凭据
return data.map((item) => `http://${item.ip}:${item.port}`);
}
const pool = await fetchProxyPool('YOUR_API_KEY', 5);
console.log(pool);
排错清单
| 现象 | 常见原因 |
|---|---|
| 请求直接绕过了代理 | 用了原生 fetch 但没设 dispatcher;axios 忘了 proxy: false |
407 | 凭据错;用户名里的 - 被误处理 |
400 | invalid_proxy_parameters:参数格式、顺序或依赖错误 |
402 | traffic_exhausted / account_quota_exhausted / quota_exhausted:流量或额度不足 |
403 | account_disabled / account_expired / access_denied / target_denied:账号或策略拒绝 |
429 | connection_limit:降低并发和建连频率 |
502 | 出口连接失败 |
503 | 暂无可用线路或服务暂不可用 |
504 | 建立代理连接超时 |
ECONNRESET 频发 | 连接池 keepAlive 超过网关的 5 分钟空闲超时 |
| IP 一直不变 | dispatcher/agent 被复用,连接没重建 |
HTTP / CONNECT 失败可读取 X-NextProxy-Error 与 JSON 的 error.code;目标网站返回的状态和 CONNECT 握手错误需分别处理。完整映射见 错误码对照。