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

如何使用Fetch以Multipart Form Data流式上传ReadableStream?

如何通过FormData实现Blob的流式上传(基于ReadableStream)

你遇到的问题本质是FormData的append方法仅支持字符串或Blob/File类型的值,传入ReadableStream会被自动转为[object ReadableStream]文本,无法实现流式上传。要解决这个问题,我们可以手动构建符合multipart/form-data格式的ReadableStream,将表单字段和文件流按协议规范拼接后作为fetch的body。

解决方案:手动构建Multipart流

核心思路是按照multipart/form-data的协议格式,把表单字段的文本内容、文件流、分隔边界(boundary)拼接成一个统一的ReadableStream,这样既满足FormData的格式要求,又能保留文件的流式传输特性。

实现代码

首先定义一个工具函数,用于生成包含表单字段和文件流的multipart流:

async function createMultipartFormDataStream(fields, fileFieldName, fileStream, fileName, mimeType = 'application/octet-stream') {
  // 生成随机分隔边界
  const boundary = `----WebKitFormBoundary${Math.random().toString(16).slice(2)}`;
  const encoder = new TextEncoder();

  // 组装表单字段的前缀内容
  const prefixParts = [];
  for (const [name, value] of Object.entries(fields)) {
    prefixParts.push(encoder.encode(`--${boundary}\r\n`));
    prefixParts.push(encoder.encode(`Content-Disposition: form-data; name="${name}"\r\n\r\n`));
    prefixParts.push(encoder.encode(`${value}\r\n`));
  }

  // 组装文件字段的头部
  prefixParts.push(encoder.encode(`--${boundary}\r\n`));
  prefixParts.push(encoder.encode(`Content-Disposition: form-data; name="${fileFieldName}"; filename="${fileName}"\r\n`));
  prefixParts.push(encoder.encode(`Content-Type: ${mimeType}\r\n\r\n`));

  // 将前缀内容转为ReadableStream
  const prefixStream = new ReadableStream({
    start(controller) {
      prefixParts.forEach(part => controller.enqueue(part));
      controller.close();
    }
  });

  // 组装尾部的结束边界
  const suffixStream = new ReadableStream({
    start(controller) {
      controller.enqueue(encoder.encode(`\r\n--${boundary}--\r\n`));
      controller.close();
    }
  });

  // 合并前缀流、文件流、尾部流
  const combinedStream = new ReadableStream({
    async start(controller) {
      // 读取并转发前缀流
      const prefixReader = prefixStream.getReader();
      let result;
      while (!(result = await prefixReader.read()).done) {
        controller.enqueue(result.value);
      }

      // 读取并转发文件流
      const fileReader = fileStream.getReader();
      while (!(result = await fileReader.read()).done) {
        controller.enqueue(result.value);
      }

      // 读取并转发尾部流
      const suffixReader = suffixStream.getReader();
      while (!(result = await suffixReader.read()).done) {
        controller.enqueue(result.value);
      }

      controller.close();
    }
  }, {
    // 设置高水位线,优化流式传输性能
    highWaterMark: 64 * 1024
  });

  return { stream: combinedStream, boundary };
}

然后在你的上传逻辑中使用这个函数:

async function main() {
  const blob = new Blob([new Uint8Array(10 * 1024 * 1024)]);
  const fileStream = blob.stream();

  // 生成包含表单字段和文件流的multipart流
  const { stream: formDataStream, boundary } = await createMultipartFormDataStream(
    {
      // 这里可以添加其他表单字段,比如用户ID、备注等
      uploader: 'test_user',
      description: 'large binary file'
    },
    'file', // 文件字段名
    fileStream,
    'large_file.bin', // 文件名
    blob.type // 文件MIME类型
  );

  // 发起流式上传请求
  await fetch('YOUR_UPLOAD_ENDPOINT', {
    method: 'POST',
    body: formDataStream,
    duplex: 'half',
    headers: {
      // 手动设置Content-Type,指定boundary
      'Content-Type': `multipart/form-data; boundary=${boundary}`
    }
  });
}

原理说明

  • multipart/form-data格式通过边界字符串(boundary)分隔不同的表单字段,每个字段包含头部信息和内容体。
  • 我们将表单字段的文本内容编码为Uint8Array,转为ReadableStream;然后依次转发前缀流、文件流、尾部流,最终形成一个完整的multipart流。
  • fetch接收这个流作为body后,会按照multipart协议解析内容,服务端就能像处理普通FormData请求一样获取表单字段和文件内容。

内容的提问来源于stack exchange,提问作者Mario Ishac

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 21:55:27