- 后端
- 网络
- 通信
【免费下载链接】undici
An HTTP/1.1 client, written from scratch for Node.js
Undici 默认面向生产环境调优,会在 HTTP 请求完成后将 socket 保留数秒以复用连接,但这在自动化测试中会导致执行时间变长。本指南讲解如何在测试中收紧 Keep-Alive 超时、如何识别并防护"静默重连"对测试结果的掩盖,以及如何用disconnect守卫捕捉意外断连,帮助你在 undici 的测试代码中写出更快、更可靠、更不易漏报的用例。
1. 为什么默认配置不适合测试
Undici 是一个"从零为 Node.js 编写的 HTTP/1.1 客户端"(见 README.md),其默认行为围绕生产场景设计:一个请求完成后,socket 会被保留几秒钟(默认keepAliveTimeout为 4 秒,即4000ms,见 client.js),以便下一个请求直接复用连接,省去重新建连的开销。
在生产环境中这是巨大的性能优势,但在自动化测试中却是负担:
- 每个测试用例结束时,空闲 socket 仍占用着文件描述符与事件循环资源;
- 测试进程可能因为未关闭的连接而迟迟无法退出,或者需要额外等待超时;
- 多个测试共享长连接时,偶发状态会跨用例泄漏,导致用例间相互干扰。
因此,测试场景下通常需要把 Keep-Alive 窗口压缩到极小,让 socket 几乎在响应完成后立即释放。
2. 测试环境推荐配置:10ms Keep-Alive
官方推荐的做法是创建一个专门用于测试的Agent,将 socket 的保持时间压到 10ms:
import { request, setGlobalDispatcher, Agent } from 'undici' const agent = new Agent({ keepAliveTimeout: 10, // milliseconds keepAliveMaxTimeout: 10 // milliseconds }) setGlobalDispatcher(agent)这样request()、fetch()等所有走全局调度器的请求都会使用这个测试 Agent。设置后,每个请求完成后 socket 最多被保留 10ms 即被回收,测试进程更容易干净退出,整体执行时间显著缩短。
2.1 相关参数的实际含义与默认值
从 Client 构造函数 与类型定义可以看出,与 Keep-Alive 相关的参数在构造Agent/Client时均受支持,具体语义如下(对应文档见 Client.md):
| 参数 | 默认值 | 含义 |
|---|---|---|
keepAliveTimeout | 4000(4s) | 空闲 socket 在无请求时可被保留的最长时间(毫秒)。若响应中服务器未给出keep-alive提示,使用该值 |
keepAliveMaxTimeout | 600000(10min) | keepAliveTimeout可达到的上限。若服务器通过keep-alive响应头给出了更大的时间提示,实际值仍会被钳制在该上限内 |
keepAliveTimeoutThreshold | 2000(2s) | 从服务器keep-alive提示中减去的毫秒数,用于留出缓冲、避免恰好撞上服务端超时 |
默认值来源见 client.js。在 H1 实现的响应头解析处(client-h1.js),最终生效的超时被计算为:
timeout = Math.min(serverKeepAliveHint - keepAliveTimeoutThreshold, keepAliveMaxTimeout)即:先按服务器的 keep-alive 提示减去阈值,再受keepAliveMaxTimeout钳制。测试中把两者都设为 10ms,意味着无论服务器提示什么,socket 都会在 10ms 内释放。
2.2 参数校验规则
构造函数会对这些参数做严格校验(client.js):
keepAliveTimeout与keepAliveMaxTimeout必须是非null的有限数字,且必须> 0,否则抛出InvalidArgumentError('invalid keepAliveTimeout')或'invalid keepAliveMaxTimeout';keepAliveTimeoutThreshold必须是有限数字;- 旧版参数名
keepAlive、idleTimeout、maxKeepAliveTimeout已不再支持,传入会分别抛出 "use pipelining=0 instead"、"use keepAliveTimeout instead"、"use keepAliveMaxTimeout instead" 的InvalidArgumentError(见 client.js)。
因此上述 10ms 配置不仅合法,而且恰好位于最小合法值之上,是测试场景下的合理选择。
3. 用 disconnect 守卫捕捉"意外断连"
3.1 问题的本质:静默重连会掩盖 Bug
Undici 的Client在 socket 出错后会自动重连(见 Client.md 中'disconnect'事件说明:"The client reconnects if or onceclient.size > 0")。
这在生产中是健壮性设计,但在测试中却是陷阱:假设服务器返回了畸形响应、解析器报错、或协议被违反,Client会悄悄断开 socket 再重连,下一次请求照常成功——测试可能"意外地"仍然通过。这类断连恰好是测试最应该暴露的问题,却被静默重连机制掩盖了。
3.2 守卫代码:在非关闭状态下断连即失败
要捕捉这类问题,可在创建Client后立即挂上disconnect事件守卫:
const { Client } = require('undici') const { test, after } = require('node:test') const { tspl } = require('@matteo.collina/tspl') test('example with disconnect guard', async (t) => { t = tspl(t, { plan: 1 }) const client = new Client('http://localhost:3000') after(() => client.close()) client.on('disconnect', () => { if (!client.closed && !client.destroyed) { t.fail('unexpected disconnect') } }) // ... test logic ... })要点解析:
client.close()与client.destroy()都会触发'disconnect'事件,但这些属于预期断连。守卫通过!client.closed && !client.destroyed把它们排除在外;- 只有在测试活跃期间(client 既未 close 也未 destroy)发生的断连才会调用
t.fail('unexpected disconnect'),让测试立即失败; - 该模式在 undici 自己的测试库中被广泛使用,例如 client-reconnect.js、client-pipelining.js、client-stream.js 等均采用
if (!client.closed && !client.destroyed) t.fail('unexpected disconnect')的写法。
3.3 底层实现:disconnect 事件从哪里来
从源码看,'disconnect'事件在以下路径被发出:
- H1 连接在 socket 关闭/出错时(client-h1.js):
client.emit('disconnect', client[kUrl], [client], err),err为导致断连的错误(如SocketError); - 成功 upgrade(WebSocket/
upgrade请求)后 socket 从Client剥离时(client-h1.js),携带InformationalError('upgrade'); - H2 连接关闭时(client-h2.js)。
因此监听client.on('disconnect', (origin, targets, err) => {})可以拿到:origin(URL)、targets(受影响的 dispatcher 数组)和err(断连原因)。守卫判断"client 是否仍在服务状态",即可区分预期断连与意外断连。
3.4 仓库中的现成实现:h2-disconnect-guard
仓库测试工具里已经封装了一个更完整的守卫:test/utils/h2-disconnect-guard.js中的guardAgainstUnexpectedDisconnect(t, client)(见 h2-disconnect-guard.js):
function guardAgainstUnexpectedDisconnect (t, client) { const onDisconnect = (_url, _targets, err) => { if (client.closed || client.destroyed) { return } if (err != null && SELF_INFLICTED_DISCONNECTS.has(err.message)) { return } t.fail(`unexpected disconnect: ${err?.message ?? 'no error'}`) } client.on('disconnect', onDisconnect) return () => client.off('disconnect', onDisconnect) }它在基础守卫之上增加了一个豁免集合SELF_INFLICTED_DISCONNECTS,当前包含'socket idle timeout':当 H2 socket 因自身 Keep-Alive 空闲超时被Client主动丢弃时(这是测试自己"造成的"断连,而非对端强制的),守卫不会误报。它返回一个移除守卫的函数,便于在用例结束时off掉监听。使用示例见 http2-connection.js 与 h2-disconnect-guard.js。
4. 什么时候应当跳过守卫
守卫的目的是捕捉"测试没有主动要求、却发生了的断连"。如果某个测试本身就预期会发生断连,继续挂守卫只会产生误报,此时应跳过守卫。典型场景包括:
- Signal 中止:
signal.emit('abort')、ac.abort()触发请求取消; - 服务端主动销毁:
res.destroy()、req.socket.destroy(); - 请求体中途被客户端销毁:
data.body.destroy(); - 超时错误:
HeadersTimeoutError、BodyTimeoutError引发的连接关闭; - 成功 upgrade:socket 被从
Client剥离(如 WebSocket 握手成功后,见上文InformationalError('upgrade')的发出路径); - 重试/重连类测试:断连本身就是触发重试的机制,属于被测行为的一部分;
- 畸形响应的 HTTP 解析错误:
HTTPParserError——这类测试的意图就是验证解析错误处理,断连是预期结果。
判断口诀:只有当"连接存活状态(未 close、未 destroy)下的断连"会让测试结果失真时,才需要守卫;凡是断连就是被测对象本身的预期行为时,请跳过。
5. 综合实战模板
将两部分结合起来,一个"快速 + 可靠"的测试用例骨架如下:
import { test, after } from 'node:test' import { Client, Agent, setGlobalDispatcher, request } from 'undici' // 1) 收紧全局 Keep-Alive,避免长连接拖慢测试 const agent = new Agent({ keepAliveTimeout: 10, keepAliveMaxTimeout: 10 }) setGlobalDispatcher(agent) test('HTTP 请求不出现意外断连', async (t) => { const client = new Client('http://localhost:3000') after(() => client.close()) // 2) 挂上 disconnect 守卫 client.on('disconnect', () => { if (!client.closed && !client.destroyed) { t.fail('unexpected disconnect') } }) // 3) 正常测试逻辑 const { statusCode } = await request('http://localhost:3000', { dispatcher: client }) t.assert.equal(statusCode, 200) })在真实仓库中,可参考 client-reconnect.js 这类同时涉及断连与重连的测试,观察守卫与预期断连分支如何共存。这样写出的测试,既保持了 Undici 生产配置所没有的"短平快",又能第一时间暴露解析器错误、协议违规等被静默重连掩盖的深层问题。
- 后端
- 网络
- 通信
【免费下载链接】undici
An HTTP/1.1 client, written from scratch for Node.js
相关推荐
TypeSpec 扩展开发指南:为诊断(Diagnostic)编写 Code Fix 与自动化测试
TypeSpec 扩展开发指南:为诊断(Diagnostic)编写 Code Fix 与自动化测试 导读 Code fix(代码修复)是 TypeSpec 编译
编程语言编译器后端navi测试编写:自动化测试开发完全指南
navi测试编写:自动化测试开发完全指南 navi作为一个功能强大的交互式命令行备忘单工具,其测试框架的设计同样体现了高效和实用的理念。本文将带你深入了解nav
开发工具chsrc测试框架:如何编写和维护自动化测试
chsrc测试框架:如何编写和维护自动化测试 chsrc作为全平台命令行换源工具,其测试框架是确保软件质量和稳定性的关键。通过精心设计的自动化测试,chsrc能
CLI开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考