☰
undici 测试编写指南:为自动化测试优化 Keep-Alive 与断开重连防护
2026/9/28 7:06:14 网站建设 项目流程
  • 后端
  • 网络
  • 通信

【免费下载链接】undici

An HTTP/1.1 client, written from scratch for Node.js

项目地址:https://gitcode.com/gh_mirrors/un/undici
点击查看免费下载

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):

参数默认值含义
keepAliveTimeout4000(4s)空闲 socket 在无请求时可被保留的最长时间(毫秒)。若响应中服务器未给出keep-alive提示,使用该值
keepAliveMaxTimeout600000(10min)keepAliveTimeout可达到的上限。若服务器通过keep-alive响应头给出了更大的时间提示,实际值仍会被钳制在该上限内
keepAliveTimeoutThreshold2000(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

项目地址:https://gitcode.com/gh_mirrors/un/undici
点击查看免费下载

相关推荐

上一篇:Heya实战教程:5步创建完美的用户引导邮件序列
下一篇:SkyPilot 在 EKS 上使用 IAM Role 访问 S3:EKS Pod Identity 与 IRSA 完整配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询