undici
为 Node.js 从头开始编写的 HTTP/1.1 客户端。
Undici 在意大利语中意为十一。1.1 -> 11 -> Eleven -> Undici。 这也是对《怪奇物语》的引用。
如何参与
对使用 Undici 有疑问?开启一个问答讨论或加入我们的官方 OpenJS Slack 频道。
想要贡献?从阅读贡献指南开始。
安装
npm i undici
基准测试
基准测试是一个简单的获取数据示例,使用 50 个 TCP 连接,管道深度为 10,运行在 Node 20.10.0 上。
测试 | 样本数 | 结果 | 误差范围 | 与最慢的差异 |
---|---|---|---|---|
undici - fetch | 30 | 3704.43 请求/秒 | ± 2.95 % | - |
http - 无保持连接 | 20 | 4275.30 请求/秒 | ± 2.60 % | + 15.41 % |
node-fetch | 10 | 4759.42 请求/秒 | ± 0.87 % | + 28.48 % |
request | 40 | 4803.37 请求/秒 | ± 2.77 % | + 29.67 % |
axios | 45 | 4951.97 请求/秒 | ± 2.88 % | + 33.68 % |
got | 10 | 5969.67 请求/秒 | ± 2.64 % | + 61.15 % |
superagent | 10 | 9471.48 请求/秒 | ± 1.50 % | + 155.68 % |
http - 保持连接 | 25 | 10327.49 请求/秒 | ± 2.95 % | + 178.79 % |
undici - pipeline | 10 | 15053.41 请求/秒 | ± 1.63 % | + 306.36 % |
undici - request | 10 | 19264.24 请求/秒 | ± 1.74 % | + 420.03 % |
undici - stream | 15 | 20317.29 请求/秒 | ± 2.13 % | + 448.46 % |
undici - dispatch | 10 | 24883.28 请求/秒 | ± 1.54 % | + 571.72 % |
基准测试是一个简单的发送数据示例,使用 50 个 TCP 连接,管道深度为 10,运行在 Node 20.10.0 上。
测试 | 样本数 | 结果 | 误差范围 | 与最慢的差异 |
---|---|---|---|---|
undici - fetch | 20 | 1968.42 请求/秒 | ± 2.63 % | - |
http - 无保持连接 | 25 | 2330.30 请求/秒 | ± 2.99 % | + 18.38 % |
node-fetch | 20 | 2485.36 请求/秒 | ± 2.70 % | + 26.26 % |
got | 15 | 2787.68 请求/秒 | ± 2.56 % | + 41.62 % |
request | 30 | 2805.10 请求/秒 | ± 2.59 % | + 42.50 % |
axios | 10 | 3040.45 请求/秒 | ± 1.72 % | + 54.46 % |
superagent | 20 | 3358.29 请求/秒 | ± 2.51 % | + 70.61 % |
http - 保持连接 | 20 | 3477.94 请求/秒 | ± 2.51 % | + 76.69 % |
undici - pipeline | 25 | 3812.61 请求/秒 | ± 2.80 % | + 93.69 % |
undici - request | 10 | 6067.00 请求/秒 | ± 0.94 % | + 208.22 % |
undici - stream | 10 | 6391.61 请求/秒 | ± 1.98 % | + 224.71 % |
undici - dispatch | 10 | 6397.00 请求/秒 | ± 1.48 % | + 224.98 % |
快速开始
import { request } from 'undici'
const {
statusCode,
headers,
trailers,
body
} = await request('http://localhost:3000/foo')
console.log('收到响应', statusCode)
console.log('headers', headers)
for await (const data of body) { console.log('data', data) }
console.log('trailers', trailers)
Body Mixins
Body mixins 是格式化请求/响应体的最常见方式。Mixins 包括:
[!注意]
undici.request
返回的 body 不实现.formData()
。
使用示例:
import { request } from 'undici'
const {
statusCode,
headers,
trailers,
body
} = await request('http://localhost:3000/foo')
console.log('收到响应', statusCode)
console.log('headers', headers)
console.log('data', await body.json())
console.log('trailers', trailers)
注意:一旦调用了一个 mixin,body 就不能被重用,因此在 .body
上调用额外的 mixins,例如 .body.json(); .body.text()
将导致抛出 TypeError: unusable
错误并通过 Promise
拒绝返回。
如果在使用 mixin 后需要以纯文本形式访问 body
,最佳做法是首先使用 .text()
mixin,然后手动将文本解析为所需的格式。
有关它们行为的更多信息,请参考 Fetch 标准中的 body mixin。
常用 API 方法
本节记录了我们最常用的 API 方法。其他 API 在 docs 文件夹中的单独文件中有文档说明,可以通过文档网站左侧的导航列表访问。
undici.request([url, options]): Promise
参数:
- url
string | URL | UrlObject
- options
RequestOptions
- dispatcher
Dispatcher
- 默认:getGlobalDispatcher - method
String
- 默认:如果有options.body
则为PUT
,否则为GET
- dispatcher
返回一个带有 Dispatcher.request
方法结果的 promise。
调用 options.dispatcher.request(options)
。
更多详情见 Dispatcher.request,示例见 request examples。
undici.stream([url, options, ]factory): Promise
参数:
- url
string | URL | UrlObject
- options
StreamOptions
- dispatcher
Dispatcher
- 默认:getGlobalDispatcher - method
String
- 默认:如果有options.body
则为PUT
,否则为GET
- dispatcher
- factory
Dispatcher.stream.factory
返回一个带有 Dispatcher.stream
方法结果的 promise。
调用 options.dispatcher.stream(options, factory)
。
更多详情见 Dispatcher.stream。
undici.pipeline([url, options, ]handler): Duplex
参数:
- url
string | URL | UrlObject
- options
PipelineOptions
- dispatcher
Dispatcher
- 默认:getGlobalDispatcher - method
String
- 默认:如果有options.body
则为PUT
,否则为GET
- dispatcher
- handler
Dispatcher.pipeline.handler
返回:stream.Duplex
调用 options.dispatch.pipeline(options, handler)
。
更多详情见 Dispatcher.pipeline。
undici.connect([url, options]): Promise
使用 HTTP CONNECT 与请求的资源启动双向通信。
参数:
- url
string | URL | UrlObject
- options
ConnectOptions
- dispatcher
Dispatcher
- 默认:getGlobalDispatcher
- dispatcher
- callback
(err: Error | null, data: ConnectData | null) => void
(可选)
返回一个带有 Dispatcher.connect
方法结果的 promise。
调用 options.dispatch.connect(options)
。
更多详情见 Dispatcher.connect。
undici.fetch(input[, init]): Promise
实现 fetch。
- https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
- https://fetch.spec.whatwg.org/#fetch-method
基本使用示例:
import { fetch } from 'undici'
const res = await fetch('https://example.com')
const json = await res.json()
console.log(json)
你可以向 fetch
传递一个可选的 dispatcher:
import { fetch, Agent } from 'undici'
const res = await fetch('https://example.com', {
// 也支持 Mocks
dispatcher: new Agent({
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10
})
})
const json = await res.json()
console.log(json)
request.body
body 可以是以下类型之一:
- ArrayBuffer
- ArrayBufferView
- AsyncIterables
- Blob
- Iterables
- String
- URLSearchParams
- FormData
在这个 fetch 实现中,request.body
现在接受 Async Iterables
。这在 Fetch 标准中并不存在。
import { fetch } from 'undici'
const data = {
async *[Symbol.asyncIterator]() {
yield 'hello'
yield 'world'
},
}
await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
FormData 除了文本数据和缓冲区外,还可以通过 Blob 对象使用流:
import { openAsBlob } from 'node:fs'
const file = await openAsBlob('./big.csv')
const body = new FormData()
body.set('file', file, 'big.csv')
await fetch('http://example.com', { method: 'POST', body })
request.duplex
'half'
在这个 fetch 实现中,如果 `request Node 中的垃圾回收不如浏览器那样积极和确定性(由于缺乏浏览器通过渲染刷新率产生的明确空闲期),这意味着将连接资源的释放留给垃圾收集器可能会导致过度的连接使用、性能降低(由于连接重用减少),甚至在用完连接时出现停滞或死锁。
// 推荐做法
const headers = await fetch(url)
.then(async res => {
for await (const chunk of res.body) {
// 强制消耗响应体
}
return res.headers
})
// 不推荐做法
const headers = await fetch(url)
.then(res => res.headers)
然而,如果你只想获取头信息,使用 HEAD
请求方法可能更好。使用这种方法可以避免消耗或取消响应体的需要。更多详情请参阅 MDN - HTTP - HTTP 请求方法 - HEAD。
const headers = await fetch(url, { method: 'HEAD' })
.then(res => res.headers)
禁止和安全列表头名称
- https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
- https://fetch.spec.whatwg.org/#forbidden-header-name
- https://fetch.spec.whatwg.org/#forbidden-response-header-name
- https://github.com/wintercg/fetch/issues/6
Fetch 标准要求实现从请求和响应中排除某些头。在浏览器环境中,某些头是被禁止的,以便用户代理保持对它们的完全控制。在 Undici 中,这些限制被移除以给用户更多控制权。
undici.upgrade([url, options]): Promise
升级到不同的协议。更多详情请参阅 MDN - HTTP - 协议升级机制。
参数:
- url
string | URL | UrlObject
- options
UpgradeOptions
- dispatcher
Dispatcher
- 默认值: getGlobalDispatcher
- dispatcher
- callback
(error: Error | null, data: UpgradeData) => void
(可选)
返回 Dispatcher.upgrade
方法的结果 Promise。
调用 options.dispatcher.upgrade(options)
。
更多详情请参阅 Dispatcher.upgrade。
undici.setGlobalDispatcher(dispatcher)
- dispatcher
Dispatcher
设置通用 API 方法使用的全局调度器。
undici.getGlobalDispatcher()
获取通用 API 方法使用的全局调度器。
返回: Dispatcher
undici.setGlobalOrigin(origin)
- origin
string | URL | undefined
设置 fetch
中使用的全局源。
如果传入 undefined
,全局源将被重置。这将导致在传入相对路径时 Response.redirect
、new Request()
和 fetch
抛出错误。
setGlobalOrigin('http://localhost:3000')
const response = await fetch('/api/ping')
console.log(response.url) // http://localhost:3000/api/ping
undici.getGlobalOrigin()
获取 fetch
中使用的全局源。
返回: URL
UrlObject
- port
string | number
(可选) - path
string
(可选) - pathname
string
(可选) - hostname
string
(可选) - origin
string
(可选) - protocol
string
(可选) - search
string
(可选)
规范遵从性
本节记录了 Undici 不支持或未完全实现的 HTTP/1.1 规范部分。
Expect
Undici 不支持 Expect
请求头字段。请求体总是立即发送,100 Continue
响应将被忽略。
参考: https://tools.ietf.org/html/rfc7231#section-5.1.1
管道
只有在配置了大于 1
的 pipelining
因子时,Undici 才会使用管道。
Undici 始终假设连接是持久的,并会立即管道化请求,而不检查连接是否持久。因此,不支持自动回退到 HTTP/1.0 或不带管道的 HTTP/1.1。
在连接失败后重试请求时,Undici 会立即管道化。然而,Undici 不会重试先前管道中剩余的第一个请求,而是将相应的回调/promise/stream 设为错误。
当管道中的任何请求被中止时,Undici 将中止所有正在运行的请求。
- 参考: https://tools.ietf.org/html/rfc2616#section-8.1.2.2
- 参考: https://tools.ietf.org/html/rfc7230#section-6.3.2
手动重定向
由于在服务器端无法手动跟随 HTTP 重定向,当使用 manual
重定向调用时,Undici 返回实际响应而不是经过 opaqueredirect
过滤的响应。这使 fetch()
与 Deno 和 Cloudflare Workers 中的其他实现保持一致。
参考: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
解决方法
网络地址族自动选择
如果在连接到远程服务器时遇到问题,该服务器由 DNS 服务器首先解析为 IPv6 (AAAA 记录),那么您的本地路由器或 ISP 可能在连接到 IPv6 网络时存在问题。在这种情况下,undici 将抛出代码为 UND_ERR_CONNECT_TIMEOUT
的错误。
如果目标服务器同时解析为 IPv6 和 IPv4 (A 记录)地址,并且您使用的是兼容的 Node 版本(18.3.0 及以上),您可以通过提供 autoSelectFamily
选项(由 undici.request
和 undici.Agent
都支持)来解决这个问题,这将在建立连接时启用地址族自动选择算法。
协作者
- Daniele Belardi, https://www.npmjs.com/~dnlup
- Ethan Arrowood, https://www.npmjs.com/~ethan_arrowood
- Matteo Collina, https://www.npmjs.com/~matteo.collina
- Matthew Aitken, https://www.npmjs.com/~khaf
- Robert Nagy, https://www.npmjs.com/~ronag
- Szymon Marczak, https://www.npmjs.com/~szmarczak
- Tomas Della Vedova, https://www.npmjs.com/~delvedor
发布者
- Ethan Arrowood, https://www.npmjs.com/~ethan_arrowood
- Matteo Collina, https://www.npmjs.com/~matteo.collina
- Robert Nagy, https://www.npmjs.com/~ronag
- Matthew Aitken, https://www.npmjs.com/~khaf
许可证
MIT