Node.js 原生 https 直连做接口自动化的五个坑

发布于2026-10-09 13:59 阅读27次 接口自动化脚本写着写着就卡住?本文用 Node.js 原生 https 模块手写一套可复用的请求骨架,逐条讲清 Content-Length 字节长度、cookie 跨请求保持、gzip 乱码、charset 与超时这五个高频坑,并附一份排查顺序清单。
## 为什么用原生 https 而不用 axios
在给老服务器写接口自动化脚本时,我一度是 axios 的重度用户。直到有一次在只有内网环境、没有 npm 源的机器上跑脚本,装依赖直接卡死;再后来遇到目标服务器对请求头指纹敏感,axios 默认带的一堆头反被拦截。从那以后,凡是简单的接口调用,我都改用 Node.js 原生 `https` 模块直连——零依赖、可完全控制请求头、行为可预测。
但原生写法有几个坑,几乎每个人都踩过。
## 坑一:Content-Length 用错长度
最常见的一行错误代码:
```js
headers['Content-Length'] = data.length; // 错!
```
`String.length` 返回的是 UTF-16 码元个数,而 `Content-Length` 要求的是**字节数**。只要请求体里有中文,两者就不相等——比如"你好"两个字符,length 是 2,字节是 6。长度声明偏小,服务端会截断请求体,于是你收到一个莫名其妙的 400,或者保存进去的内容只剩半截。
正确写法:
```js
const payload = JSON.stringify(body);
headers['Content-Length'] = Buffer.byteLength(payload, 'utf8');
```
## 坑二:Cookie 没有跨请求保持
服务器在登录接口会下发 `token` cookie,后续所有请求都必须带上。原生模块**不会自动帮你维护 cookie jar**,必须自己实现:
```js
const cookies = new Map();
function storeCookies(setCookie) {
if (!setCookie) return;
for (const line of setCookie) {
const first = line.split(';')[0];
const i = first.indexOf('=');
if (i > 0) cookies.set(first.slice(0, i).trim(), first.slice(i + 1).trim());
}
}
```
注意 `set-cookie` 响应头在 Node 里是**数组**,不是字符串,`split` 之前要先确认类型,否则拿到的是逗号拼接的整体,解析必错。
## 坑三:响应体被 gzip 成一堆乱码
默认请求头不带 `Accept-Encoding`,很多服务器仍会返回 gzip 压缩内容。如果你没解压,拿到的 buffer 转成字符串就是乱码。
两种处理:一是显式声明 `'Accept-Encoding': 'identity'` 告诉服务端不要压缩;二是用 `zlib.gunzipSync` 解压。做接口自动化我更推荐前者,简单直接。
## 坑四:中文乱码与 charset
Node 的 `res.setEncoding('utf8')` 只负责按 UTF-8 解码缓冲区,**不会改变服务端返回的内容编码**。如果对方还是 GBK 老站点,照样乱码。判断方法:读响应头 `content-type` 里有没有 `charset=`。没有时默认按 UTF-8 处理。
另外 Windows 终端输出中文前,记得设置 `PYTHONIOENCODING` 类似的思路——Node 里可以用 `process.stdout.write` 配合 `Buffer.from(str, 'utf8')` 保证管道输出不乱码。
## 坑五:没有超时,脚本静默挂死
原生 `https.request` 默认**没有超时**。对方服务器不响应时,脚本会一直挂着,你以为在跑,其实早就死了。一定要加:
```js
req.setTimeout(30000, () => req.destroy(new Error('timeout')));
```
并且把 `req.on('error')` 接上,否则异常会被静默吞掉。
## 一份可复用的最小骨架
把上面五点串起来,核心就这么多:
```js
function req(host, path, method, body) {
return new Promise((resolve, reject) => {
const payload = body === undefined ? null : JSON.stringify(body);
const headers = { 'Accept-Encoding': 'identity' };
if (payload !== null) {
headers['Content-Type'] = 'application/json;charset=UTF-8';
headers['Content-Length'] = Buffer.byteLength(payload);
}
const ch = cookieHeader();
if (ch) headers['Cookie'] = ch;
const r = https.request({ hostname: host, path, method, headers }, res => {
storeCookies(res.headers['set-cookie']);
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});
r.on('error', reject);
r.setTimeout(30000, () => r.destroy(new Error('timeout')));
if (payload !== null) r.write(payload);
r.end();
});
}
```
## 排查清单
遇到问题时按这个顺序过一遍:先确认 `Content-Length` 是不是按字节算的;再打印 `res.headers['set-cookie']` 看 cookie 有没有拿到;然后看响应头有没有 `content-encoding`;最后确认超时回调有没有生效。九成问题都在这四步里。
原生模块的好处是透明——每一个字节、每一个头都在你手里。代价是这些细节都得自己管。把这五个坑填上,它比任何第三方库都可靠。