Node.js中https.get的timeout机制及1秒超时重试实现问题
问题背景
我正在用https.get发送GET请求,想明确timeout选项的工作机制。查资料得知该选项仅控制连接超时,套接字建立后便不再生效。
我编写了带重试逻辑的getRequest函数,既在请求选项中设置timeout:1000,又调用.setTimeout(1000)试图处理连接后的超时场景:
function getRequest(url) { return new Promise((resolve, reject) => { const req = https.get(url, {timeout: 1000}, res => { let rawData = ''; res.on('data', chunk => { rawData += chunk; }); res.on('end', () => { resolve(rawData); }); }).setTimeout(1000); req.on('error', err => { reject(new Error(err)) }) req.on('timeout', () => { req.end(); getRequest(url); }) }); }
测试发现,100次请求中至少有一次耗时恰好5秒,推测是某个默认配置导致。我需要确保连接在1秒后立即关闭并发起重试。另外尝试node-fetch、axios等带超时功能的模块时,也遇到类似问题:Axios会抛出超时错误,但Node.js应用要等待5秒才结束。
问题根源
这个5秒延迟来自Node.js底层的套接字默认超时(socket.setTimeout()默认值为5000ms)。即便给请求设置了1秒超时,当套接字已建立但服务器迟迟不返回数据时,底层套接字的默认5秒超时才会触发,导致整个请求拖到5秒才结束。
同时你的重试逻辑存在问题:在timeout事件中直接调用getRequest(url),但递归调用的Promise未与外层Promise关联,外层Promise会一直处于pending状态,直到底层套接字超时才会有结果。
解决方案
1. 覆盖套接字默认超时
在请求的socket事件触发时,手动设置套接字超时时间,替换默认的5秒:
function getRequest(url) { return new Promise((resolve, reject) => { const req = https.get(url, { timeout: 1000 }, res => { let rawData = ''; res.on('data', chunk => { rawData += chunk; }); res.on('end', () => { resolve(rawData); }); }); // 覆盖套接字默认超时 req.on('socket', (socket) => { socket.setTimeout(1000); socket.on('timeout', () => { req.destroy(new Error('Socket timeout')); }); }); req.on('timeout', () => { req.destroy(new Error('Request timeout')); }); req.on('error', err => { // 将重试的Promise结果关联到外层Promise resolve(getRequest(url)); }); }); }
2. 正确关联重试Promise
将重试调用的getRequest(url)通过resolve()传递给外层Promise,避免外层Promise一直挂起。
3. 用destroy()替代end()
req.end()仅结束请求但不会强制关闭套接字,改用req.destroy()可立即终止连接,避免等待底层套接字的默认超时。
4. 限制重试次数(可选)
为避免无限重试,可添加重试次数参数:
function getRequest(url, retries = 3) { return new Promise((resolve, reject) => { const req = https.get(url, { timeout: 1000 }, res => { let rawData = ''; res.on('data', chunk => { rawData += chunk; }); res.on('end', () => { resolve(rawData); }); }); req.on('socket', (socket) => { socket.setTimeout(1000); socket.on('timeout', () => { req.destroy(new Error('Socket timeout')); }); }); req.on('timeout', () => { req.destroy(new Error('Request timeout')); }); req.on('error', err => { if (retries > 0) { resolve(getRequest(url, retries - 1)); } else { reject(new Error(`重试${3 - retries}次后失败: ${err.message}`)); } }); }); }
关键说明
https.get的timeout选项仅负责连接阶段的超时(DNS解析、TCP握手),连接建立后失效。- 必须通过
socket事件设置套接字超时,才能覆盖默认5秒,确保数据传输阶段也能在1秒超时。 destroy()会立即关闭连接并触发error事件,让重试逻辑及时执行,无需等待底层5秒超时。
内容的提问来源于stack exchange,提问作者macaw2980

