
用 Node 的 fetch() 发送 ReadableStream 时,代码看起来只差一个配置项,结果却差在请求有没有真正发出。实测省略 duplex 会在客户端直接抛 TypeError,本机服务端一次请求也没收到;加上 duplex: 'half' 后,服务端收到完整正文并返回 200。
这和普通字符串请求体不一样。字符串已经有确定内容和长度,fetch 可以直接处理;流的长度与结束时刻要等读取过程推进,Node 要求调用方明确声明 duplex。这个字段并不把请求变成双向全双工传输,当前这里使用的值就是 half。
少了配置,服务端看不到请求
脚本用 ReadableStream 分两次 enqueue 文本,再关闭流。第一次调用没有 duplex,捕获到 TypeError。随后立即检查服务端记录,计数仍为 0。错误发生在客户端这边,不是服务器收到请求后拒绝了数据。
加上 duplex: 'half' 再发送同样的流,客户端拿到 200 和 ok。服务端将收到的字节拼起来后得到 hello stream,UTF-8 长度为 12 字节。这个结果证明请求体到达了服务端;它没有测量两个 enqueue 分别以几个 TCP 包到达,不能据此推断网络分片方式。
配置项写在 RequestInit 里,不会变成一个 HTTP 请求头,也不会替服务端声明如何读取数据。示例中的 makeBody() 每次都会新建一支流,因为流被消费后不能像字符串那样直接再读一遍。重试上传时要重新创建流,或让调用方提供新的数据源;已经读完的那支流不能当成可重复使用的请求体。
脚本服务端等到 end 后才回复。它验证的是 Node 接受该请求体并把字节交给本机服务端,不是边上传边收到服务端响应。Node 的 Fetch 这里要求 duplex: 'half',所以字段名里的 duplex 容易让人联想到完整双向传输;实际实验没有测出这种结论,也没有证明流的分块抵达边界。
node v26.8.1
missing duplex: TypeError: RequestInit: duplex option is required when sending a body.
server requests after missing duplex: 0
duplex=half: status=200 response=ok
server received: method=POST body="hello stream" bytes=12
配置要放在请求选项里
最小写法如下。实际上传时可以把 makeBody() 换成读取文件或生成内容的 ReadableStream,但 duplex 仍要保留:
const response = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/octet-stream' },
body: readableStream,
duplex: 'half',
});
如果把流先读成 Buffer 再传,行为与直接传 ReadableStream 不同,也会失去边读边发送的用途。选择流通常是为了不把完整文件一次性放进内存;省掉 duplex 并不是一种兼容回退方案,Node 会在发送前拒绝。
duplex: 'half' 是 Node Fetch 对流式请求体的选项要求,不是服务端对请求方法的描述,也不表示响应一定要等客户端全部处理完。要核对的是 Node 的具体 Fetch 行为和服务端实际收到了什么。本次测试用的是 Node v26.8.1 与本机回环 HTTP 服务,没有请求外部站点;其他运行时是否接受相同参数,这里没实测。
字符串请求体不该照搬这条限制
只有请求体本身是 ReadableStream 时,才需要按本次观察处理这项配置。普通字符串或 Buffer 已经是完整值,可以直接交给 fetch;不要为了形式统一就给所有请求都加一个看起来像传输模式的字段。我的取舍是把构造流式请求的代码单独封装,并在封装里固定写上 duplex: 'half',这样遗漏会在代码入口被挡住。
脚本只验证了本机 Node 的行为:省略时服务端没有收到请求,带上选项后收到预期字节。它没有测浏览器里的 fetch,也没有对外部服务做上传。涉及大文件时,还应单独测取消请求后的流清理、服务端提前断开连接,以及上游响应失败时的资源回收,这些不由这组输出回答。