You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 01:22:01