如何在Node.js中用原生fetch流式传输multipart/form-data文件?
Node.js原生fetch流式上传multipart/form-data文件
目前Node.js原生的FormData(从v18开始内置)不支持直接传入fs.ReadStream作为表单字段值——直接传的话,流会被序列化为[object ReadStream]字符串,导致上传内容错误。不过可以通过以下两种原生方式实现流式上传,无需将整个文件读入内存:
方法一:Node.js v18.11+ 推荐方案
从Node.js v18.11开始,Blob构造函数支持传入异步迭代器(包括fs.ReadStream),我们可以用Blob包装文件流,再传入FormData:
const { createReadStream } = require('fs'); const { Blob } = require('buffer'); async function upload() { const formData = new FormData(); formData.append('user_id', 42); // 用Blob包装文件流,不会一次性加载整个文件到内存 const fileStream = createReadStream('./music.mp3'); // 指定文件MIME类型和文件名(可选,但部分API需要) const fileBlob = new Blob(fileStream, { type: 'audio/mpeg' }); formData.append('audio', fileBlob, 'music.mp3'); const response = await fetch('http://api.example.com', { method: 'POST', body: formData, }); // 处理响应 const result = await response.json(); console.log(result); } upload().catch(console.error);
原理说明
Blob会异步消费文件流的数据,当fetch发送请求时,会逐步将流中的数据上传,不会将整个文件加载到内存中,完美实现流式上传。
方法二:兼容低版本Node.js(<18.11)
如果你的Node.js版本低于18.11,可以手动构造multipart/form-data格式的可读流,直接作为fetch的请求体:
const { createReadStream, statSync } = require('fs'); const { Readable } = require('stream'); async function upload() { const boundary = `----${Date.now().toString(16)}`; const filePath = './music.mp3'; const fileStats = statSync(filePath); // 构造表单的各个组成部分 const formParts = [ // user_id字段的头部和内容 Readable.from(`--${boundary}\r\nContent-Disposition: form-data; name="user_id"\r\n\r\n42\r\n`), // audio文件字段的头部 Readable.from(`--${boundary}\r\nContent-Disposition: form-data; name="audio"; filename="music.mp3"\r\nContent-Type: audio/mpeg\r\nContent-Length: ${fileStats.size}\r\n\r\n`), // 文件流 createReadStream(filePath), // 表单结束边界 Readable.from(`\r\n--${boundary}--\r\n`) ]; // 合并所有流为一个可读流 const formStream = Readable.from(async function* () { for (const part of formParts) { yield* part; } }()); // 计算总内容长度(可选,但设置后能提升服务器处理性能) const contentLength = Buffer.byteLength(`--${boundary}\r\nContent-Disposition: form-data; name="user_id"\r\n\r\n42\r\n`) + Buffer.byteLength(`--${boundary}\r\nContent-Disposition: form-data; name="audio"; filename="music.mp3"\r\nContent-Type: audio/mpeg\r\nContent-Length: ${fileStats.size}\r\n\r\n`) + fileStats.size + Buffer.byteLength(`\r\n--${boundary}--\r\n`); const response = await fetch('http://api.example.com', { method: 'POST', headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}`, 'Content-Length': contentLength.toString() }, body: formStream, }); // 处理响应 const result = await response.json(); console.log(result); } upload().catch(console.error);
注意事项
- 需要手动处理
multipart/form-data的边界、字段头部和结束标记,格式必须严格符合规范,否则服务器无法解析。 - 计算
Content-Length是可选的,但建议设置,避免服务器使用分块传输编码,提升传输效率。
内容的提问来源于stack exchange,提问作者danneu
相关产品推荐
相关产品推荐

