完成一次 NodeJS HTTP 代理请求可以完全不依赖第三方包:NodeJS 内置的 http 模块本身就实现了正向代理协议。对 http 目标,把完整 URL 写进请求行的绝对形式即可;对 https 目标,先向代理发送 CONNECT 请求建立隧道,再在隧道内完成 TLS 握手。本文聚焦这套零依赖的原生实现,并补充 fetch 时代的全局代理方案,适合追求极简部署或需要精细控制连接的场景。
原生 http 模块与代理对话的原理
正向代理依据请求行中的绝对 URI 与 Host 头决定转发目标,因此客户端要做的只有两件事:连接代理端口,以及把请求行改写成「方法 完整URL 协议版本」的形式。HTTPS 报文无法明文转发,代理协议规定客户端先发送 CONNECT host:port 请求,代理与目标建立 TCP 隧道后返回 200,随后的字节流由客户端与目标服务器直接协商 TLS。理解这两条规则后,任何语言的手写代理实现都不再神秘。
完整代码示例(HTTP 目标)
下面的代码没有使用任何第三方库,保存后直接 node proxy.js 即可运行:
const http = require('http');
const req = http.request({
host: '127.0.0.1', // 代理服务器地址
port: 8080, // 代理端口
path: 'http://httpbin.org/ip', // 绝对形式的目标 URL
method: 'GET',
headers: { Host: 'httpbin.org' }
}, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
console.log('状态码:', res.statusCode);
console.log(body);
});
});
req.setTimeout(10000, () => req.destroy(new Error('连接代理超时')));
req.on('error', (err) => console.error('错误:', err.message));
req.end();
返回 200 说明代理链路正常。若代理要求认证,在 headers 中加入 Proxy-Authorization,值为 Basic 拼接 Base64 编码后的「用户名:密码」,例如使用 Buffer.from('user:pass').toString('base64') 生成编码段。
HTTPS 目标的 CONNECT 隧道写法
当目标是 https 站点时,需要先建隧道再做 TLS 握手,原生写法如下:
const http = require('http');
const tls = require('tls');
const req = http.request({
host: '127.0.0.1',
port: 8080,
method: 'CONNECT',
path: 'httpbin.org:443' // 隧道目标
});
req.on('connect', (res, socket) => {
if (res.statusCode !== 200) {
console.error('代理拒绝建立隧道:', res.statusCode);
socket.destroy();
return;
}
const tlsSocket = tls.connect(
{ socket, servername: 'httpbin.org' },
() => {
tlsSocket.write(
'GET /ip HTTP/1.1\r\n' +
'Host: httpbin.org\r\n' +
'Connection: close\r\n\r\n'
);
}
);
let body = '';
tlsSocket.setEncoding('utf8');
tlsSocket.on('data', (chunk) => { body += chunk; });
tlsSocket.on('end', () => console.log(body.split('\r\n\r\n').pop()));
tlsSocket.on('error', (err) => console.error('TLS 错误:', err.message));
});
req.on('error', (err) => console.error('CONNECT 失败:', err.message));
req.end();
http 模块在方法为 CONNECT 且代理响应 2xx 时触发 connect 事件,回调中的 socket 就是隧道端点。tls.connect 复用该 socket 完成 TLS 握手,之后手写 HTTP/1.1 请求报文即可通信。生产代码中建议基于这个 socket 构造 https.Agent,以获得更完整的请求语义与错误处理。
fetch 时代与全局代理配置
Node 18 起内置的 fetch 由 undici 驱动,它与原生 http 模块一样不会读取 HTTP_PROXY 环境变量。想让全局 fetch 默认走代理,安装 undici 包并调用 setGlobalDispatcher:
const { setGlobalDispatcher, ProxyAgent } = require('undici');
setGlobalDispatcher(new ProxyAgent('http://user:[email protected]:8080'));
fetch('https://httpbin.org/ip')
.then((res) => res.json())
.then((data) => console.log(data));
配置完成后,整个进程的 fetch 请求都会经代理发出;ProxyAgent 也支持传入 token 选项应对更复杂的认证方式。相比逐个请求传 agent,全局方案更适合统一出口的中小型服务。
常见报错与注意事项
- 解析类报错:代理返回了 HTML 拒绝页,检查请求行是否用了绝对形式、Host 头是否正确。
- CONNECT 返回 403 或 407:代理仅放行特定端口或要求认证,补充 Proxy-Authorization 头。
- 连接长时间挂起:务必用 setTimeout 主动断开,避免请求无限等待占用句柄。
- 并发场景:为每个代理维护独立连接,避免隧道串用导致响应错乱。
Article ID: 39
(本文仅供技术交流,请遵守法律法规)