undici fetch 完整指南:WHATWG Fetch 在 Node.js 中的标准落地实现

原创2026-09-26 10:06:291,623 阅读
文章标签:后端网络通信

undici fetch 完整指南:WHATWG Fetch 在 Node.js 中的标准落地实现

undici 从零实现了 WHATWG Fetch Standard],为 Node.js 提供了与浏览器行为一致的 fetch()、Request、Response、Headers、FormData 全套 API。本文以仓库官方文档 [Fetch.md 为核心骨架,结合 lib/web/fetch 目录下的真实实现源码与 test/fetch 测试用例,系统讲解每个类的构造方式、参数语义、底层原理与安全边界,帮助你在 Node.js 服务端场景中正确、安全地使用这套 API。

fetch():发起一次网络请求

undici 导出的 fetch() 完全遵循 WHATWG Fetch Standard,因此浏览器端的 MDN 文档同样适用于这里。最基本的用法与浏览器一致:

import { fetch, Request, Response, Headers, FormData } from 'undici'

const response = await fetch('https://example.com')
const text = await response.text()

在源码层面,fetch() 的入口定义在 lib/web/fetch/index.js#L160:它会先用 new Request(input, init) 构造请求对象,随后通过 fetching()(lib/web/fetch/index.js#L407)进入完整的 fetching 流程,包括重定向、凭据、缓存模式、CORS 检查等标准步骤。fetch 与 Fetch、fetching、finalizeAndReportTiming 一起从 lib/web/fetch/index.js#L2448 导出,最终由 index-fetch.js 打包对外暴露。

混合实现会抛错

Request、Response、Headers、FormData 这些类是有品牌(brand)检查的,webidl 层的 brandCheck 会校验对象是否来自同一个实现。因此:

  • 全局 fetch() 搭配全局 FormData、Request、Response、Headers;
  • undici 的 fetch() 搭配 undici 导出的同名类。

把一个实现创建的值传给另一个实现的 API 会抛出异常。这一点在 lib/web/fetch/body.js、lib/web/fetch/headers.js 中都能看到对应的 brand check 逻辑。

fetch(input[, init]) 参数详解

fetch() 的第一个参数 input 可以是字符串、URL 或 Request 实例:

  • 字符串或 URL 直接作为请求目标地址;
  • Request 实例则作为请求模板,init 中的字段会覆盖模板上的对应字段。

init 是一个 RequestInit 选项对象,完整字段如下表:

字段 类型 默认值 说明
body string | Buffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream | null null 请求体
cache string — 缓存模式:'default'、'force-cache'、'no-cache'、'no-store'、'only-if-cached'、'reload'
credentials string — 凭据发送方式:'omit'、'include'、'same-origin'
dispatcher Dispatcher 全局 dispatcher 执行请求的分发器,如 Agent、ProxyAgent
duplex string — 请求的 duplex 模式;提供流式 body 时必须为 'half'
headers Headers | Object | Array — 请求头,可为 Headers 实例、普通对象或 [name, value] 数组
integrity string — 请求的子资源完整性(SRI)元数据
keepalive boolean false 连接是否可以比创建它的页面活得更久
method string — 请求方法,如 'GET'、'POST'
mode string — 请求模式:'cors'、'navigate'、'no-cors'、'same-origin'
redirect string — 重定向处理:'error'、'follow'、'manual'
referrer string — 请求 referrer
referrerPolicy string — 参见下方取值列表
signal AbortSignal | null null 用于中止请求的信号
window null — 只能为 null,标准保留字段

其中 referrerPolicy 的合法取值为:''、'no-referrer'、'no-referrer-when-downgrade'、'origin'、'origin-when-cross-origin'、'same-origin'、'strict-origin'、'strict-origin-when-cross-origin'、'unsafe-url'。

返回值与错误语义

fetch() 返回一个 Promise,在收到响应头时兑现为 Response。需要特别强调的是:

  • Promise 只在网络失败时 reject;
  • HTTP 错误状态码(如 404、500)仍然会正常兑现,因此必须检查 response.ok 来判断请求是否真正成功。
import { fetch } from 'undici'

const response = await fetch('https://example.com', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ hello: 'world' }),
})

console.log(response.status)
console.log(await response.json())

从源码看,lib/web/fetch/index.js#L160 起始处的 fetch() 会先处理已中止信号的情况(lib/web/fetch/index.js#L182),随即把请求交给 fetching 流程,最终仅当网络层失败时才 reject Promise。

自定义 dispatcher 路由请求

init.dispatcher 允许你把请求交给自定义 Dispatcher(例如带特定连接选项的 Agent 或 ProxyAgent),这在浏览器 fetch 中没有对应的能力,是 undici 服务端场景的关键扩展点:

import { fetch, Agent } from 'undici'

const response = await fetch('https://example.com', {
  dispatcher: new Agent({ connect: { rejectUnauthorized: false } }),
})

不传 dispatcher 时,实现内部通过 getGlobalDispatcher() 获取全局 dispatcher(见 lib/web/fetch/index.js#L415),可通过 setGlobalDispatcher() 替换。

Class: FormData

FormData 表示一组表单字段键值对,适合作为 fetch() 的请求体,实现同样遵循 WHATWG Fetch Standard,类定义位于 lib/web/fetch/formdata.js#L16。注意同样要遵循"同一实现混用"原则:全局 FormData 配全局 fetch(),undici 的 FormData 配 undici 的 fetch()。

new FormData()

创建一个空的 FormData 实例。传入除 undefined 以外的任何参数都会抛错——特别地,本环境不支持 HTMLFormElement 参数(浏览器里 new FormData(formElement) 的用法在这里不可用)。

字段操作方法

FormData 提供五个核心方法,语义与浏览器完全一致:

方法 签名 说明
append append(name, value[, filename]) 向已有 key 追加新值;key 不存在则新增。与 set() 不同,保留 name 上已有的其他值
delete delete(name) 删除与 name 关联的全部值
get get(name) 返回 name 关联的第一个值;没有则返回 null。类型为 string | File | null
getAll getAll(name) 返回 name 关联的全部值,为 string 与 File 组成的数组
has has(name) 返回 boolean,表示 name 是否至少有一个关联值
set set(name, value[, filename]) 为已有 key 设置新值并替换旧值;key 不存在则新增

其中 value 可为 string 或 Blob;当 value 是 Blob 时,可选的 filename 参数决定上报给服务器的文件名。

Class: Response

Response 表示对一次请求的响应,通常由 await fetch() 获得,也可以直接构造。它继承 BodyMixin,因此拥有下文 Body mixin](#body-mixin) 中的所有读取方法。类定义在 [lib/web/fetch/response.js#L29。

new Response([body[, init]])

参数 类型 默认值
body string | Buffer | Uint8Array | Blob | FormData | URLSearchParams | ReadableStream | null null
init.status number 200
init.statusText string ''
init.headers Headers | Object | Array —

静态方法:Response.error()

返回一个表示网络错误的 Response,其 type 为 'error'。源码实现见 lib/web/fetch/response.js#L36:它通过 makeNetworkError() 构造网络错误响应并以 'immutable' guard 包装。

静态方法:Response.json(data[, init])

把任意 JavaScript 值序列化为 JSON 作为响应体,并自动设置 Content-Type: application/json:

import { Response } from 'undici'

const response = Response.json({ ok: true }, { status: 201 })

init 同样支持 status(默认 200)、statusText(默认 '')与 headers。实现细节在 lib/web/fetch/response.js#L46:先 serializeJavascriptValueToJSONString(data) 序列化,再经 extractBody 提取字节并以 'application/json' 类型初始化响应。对应的参数校验与序列化行为测试见 test/fetch/response-json.js(包括对 Symbol、undefined、抛错 getter 等边界的覆盖)。

静态方法:Response.redirect(url[, status])

创建一条带 Location 头的重定向响应。status 只能是重定向状态码之一:301、302、303、307、308,默认 302。源码中重定向状态集合定义在 lib/web/fetch/constants.js#L8(<a href="https://link.gitcode.com/i/c1490bfe5f315463d7d3ce026b1c5a71" target="_blank">301, 302, 303, 307, 308]),传入其他状态码会抛 RangeError([lib/web/fetch/response.js#L71)。

实例属性

属性 类型 说明
response.clone() 方法 复制响应;若 body 已被读取或已锁定则抛 TypeError
response.type string 响应类型:'basic'、'cors'、'default'、'error'、'opaque'、'opaqueredirect'
response.url string 经重定向后的最终 URL;不可用时为空字符串
response.redirected boolean 是否经过一次或多次重定向
response.status number HTTP 状态码
response.ok boolean status 在 200–299 区间内为 true
response.statusText string 与状态码对应的状态描述文本
response.headers Headers 响应关联的 Headers 对象

Class: Request

Request 表示一次资源请求,实例可以直接传给 fetch() 代替 URL 字符串。类定义在 lib/web/fetch/request.js#L89,同样继承 BodyMixin。

new Request(input[, init])

input 可以是 URL 字符串、URL 实例或另一个 Request(作为复制模板)。init 的字段与 fetch() 的 init 完全一致,传入后会覆盖模板上的对应字段。参数校验测试见 test/fetch/request.js 开头对各类非法入参(缺参、非法类型、非法 URL 等)的覆盖。

实例属性一览

属性 类型 说明
request.clone() 方法 复制请求;body 已被读取或锁定则抛 TypeError
request.method string 请求方法,如 'GET'
request.url string 序列化后的请求 URL
request.headers Headers 请求关联的 Headers 对象
request.destination string 请求目标类型,如 ''、'image'、'script'
request.referrer string 请求 referrer,可能为 'about:client' 或 URL 字符串
request.referrerPolicy string 请求的 referrer policy
request.mode string 'cors'、'navigate'、'no-cors'、'same-origin'
request.credentials string 'omit'、'include'、'same-origin'
request.cache string 请求的缓存模式
request.redirect string 'error'、'follow'、'manual'
request.integrity string 子资源完整性元数据
request.keepalive boolean 请求是否可以比创建它的环境活得更久
request.isReloadNavigation boolean 是否为刷新导航
request.isHistoryNavigation boolean 是否为历史导航
request.signal AbortSignal 关联的中止信号
request.duplex string 恒为 'half'

Class: Headers

Headers 表示请求或响应的头列表,并提供读写方法,实现位于 lib/web/fetch/headers.js#L429。它是可迭代对象,迭代时按名称排序产出 [name, value] 对。

new Headers([init])

init 可以是 Headers 实例、普通对象,或 [name, value] 二元组数组。

实例方法

方法 签名 说明
append append(name, value) 追加一个值;header 不存在则新建,保留已有值
delete delete(name) 删除指定 header
get get(name) 返回合并后的值;不存在返回 null
has has(name) 返回 boolean
set set(name, value) 设为单一值,替换已有值
getSetCookie getSetCookie() 返回所有 Set-Cookie 头的值数组,不合并,每个 Set-Cookie 单独成串

getSetCookie() 是浏览器 Headers 上近年新增的能力,undici 中实现于 lib/web/fetch/headers.js#L613:它读取内部头列表中单独维护的 cookies 数组并按序返回,从而避免多个 Set-Cookie 被逗号合并导致语义丢失。对应行为测试见 test/fetch/headers.js#L679 与 test/fetch/cookies.js#L114。

Body mixin:读取请求/响应体

Request 与 Response 都继承 BodyMixin(实现在 lib/web/fetch/body.js)。每个消费方法都只读取 body 一次:读完一次后 bodyUsed 变为 true,再调用其他消费方法会抛 TypeError。

[!WARNING] arrayBuffer()、blob()、bytes()、formData()、json()、text() 这六个方法会在返回前把整个 body 缓冲进内存,并在适用时解码/解析负载且长期保留该表示。调用它们等于信任 body 足够小、能放进可用内存。不要用它们处理来自不可信或用户可控来源的 body;应改用 body.body 或 body.textStream() 增量处理,并施加应用自定义的大小上限。

消费方法总览

方法 返回 说明
arrayBuffer() Promise<ArrayBuffer> 包含 body 字节的 ArrayBuffer
blob() Promise<Blob> 包含 body 的 Blob
bytes() Promise<Uint8Array> 包含 body 字节的 Uint8Array
formData() Promise<FormData> 将整个 body 按 multipart/form-data 或 application/x-www-form-urlencoded 解析(已废弃,见下)
json() Promise<any> 将 body 解析为 JSON
text() Promise<string> 以 UTF-8 解码 body
textStream() ReadableStream<string> undici 扩展:以 UTF-8 解码的文本块流,而非整体缓冲(实验性,不属于 WHATWG Fetch Standard)
body ReadableStream | null body 原始流;消息无 body 时为 null
bodyUsed boolean body 被读取后为 true

bytes() 与 textStream() 的实现细节可在 lib/web/fetch/body.js#L396 与 lib/web/fetch/body.js#L405 找到:前者基于 consumeBody 把字节序列包装成 Uint8Array;后者在 body 不可用时抛 TypeError,并把解码后的文本块推入一个 ReadableStream 供消费。

formData() 的废弃与替代方案

formData() 标记为 Stability: 0 - Deprecated:它整体缓冲 body 并做 multipart 解析,而 multipart 解析存在固有安全风险,因此只能在来自可信服务器的响应上调用。对于不可信或用户可控的服务器,应使用专门的流式解析器(如 @fastify/busboy)并施加应用级限制:

import { Busboy } from '@fastify/busboy'
import { Readable } from 'node:stream'

const response = await fetch('...')
const busboy = new Busboy({
  headers: { 'content-type': response.headers.get('content-type') },
})

// Handle the events emitted by `busboy`.

Readable.fromWeb(response.body).pipe(busboy)

处理不可信 body 的推荐姿势

面对用户可控或不可信的响应体,正确做法是流式消费 + 限流:

  1. 通过 response.body(原始 ReadableStream)或 response.textStream()(文本块流)增量读取;
  2. 应用自定义大小上限,超过即中止或丢弃;
  3. 避免一次性缓冲类方法(json()、text() 等)。

这正是 lib/web/fetch/body.js 中消费方法与流式路径并存的设计意图:前者服务于可信小负载的便捷解析,后者服务于不可信负载的安全处理。

从源码看实现边界

几个值得注意的底层细节,帮助理解这套 API 的行为边界:

  • 无 body 状态码:101、204、205、304 被定义为 nullBodyStatus(lib/web/fetch/constants.js#L6),这些状态码对应的响应不允许携带 body;
  • 重定向状态码:301、302、303、307、308 构成重定向集合(lib/web/fetch/constants.js#L8),Response.redirect() 与 redirect: 'follow' 均依赖此集合;
  • 坏端口拦截:WHATWG 标准规定的一批坏端口(1、7、9、21、22、23、25 等)在 fetching 流程中被显式阻止(lib/web/fetch/constants.js#L14);
  • 入口封装:index-fetch.js 对 fetchImpl 做了 Promise 包装,并在 catch 中为错误附加当前模块的调用栈,方便在 Node.js 内联打包场景下定位问题。

总结

undici 的 fetch 家族 API 是浏览器 Fetch Standard 在 Node.js 服务端的完整落地:fetch() 负责发起请求并只对网络失败 reject,Request/Response 承载请求与响应语义并共享 BodyMixin 的消费能力,Headers 提供迭代与 Set-Cookie 安全读取,FormData 提供标准表单编码。实际编码时记住三条主线:同实现对象不可混用、HTTP 错误状态需靠 response.ok 判断、不可信 body 务必走流式路径并自行限流。需要深入钻研实现时,可依次阅读 lib/web/fetch/index.js(fetch 流程)、lib/web/fetch/body.js(Body mixin)、lib/web/fetch/response.js(Response 构造与静态方法),并对照 test/fetch 下的用例验证行为。

登录后查看全文
undici