You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Cloudflare Worker中实现与Node.js同款的流式multipart/form-data中转上传功能

如何在Cloudflare Worker中实现与Node.js同款的流式multipart/form-data中转上传功能

我之前正好碰到过这个问题!Cloudflare Worker 环境里的标准 FormData 实现和 Node.js(尤其是基于 undici 的新版 FormData)不一样,它不支持直接把 ReadableStream 作为表单字段值传入,这就是你看到类型错误的原因。不过别担心,我们可以手动构建流式的 multipart/form-data 请求体,完全实现和 Node.js 一样的流式中转效果,而且全程不会把文件内容加载到内存里,完美适配 Worker 的内存限制。


核心思路

绕过 Worker 自带的 FormData,自己构造符合 multipart/form-data 格式的数据流:

  • 生成随机的分隔边界(boundary)字符串,用来区分表单里的不同字段
  • 构造表单字段的头部信息(包含字段名、文件名、Content-Type 等)
  • 把「头部流」→「下载的文件流」→「分隔边界流」拼接成一个完整的请求体流
  • 最后带着正确的 Content-Type 头(包含边界)发起 POST 请求

完整实现代码(TypeScript)

async function streamTransfer(
  downloadUrl: string,
  uploadUrl: string,
  fieldName: string = 'file',
  fileName: string = 'transfered-file'
) {
  // 1. 发起下载请求,获取响应流
  const downloadRes = await fetch(downloadUrl);
  if (!downloadRes.ok || !downloadRes.body) {
    throw new Error(`下载失败: ${downloadRes.status} ${downloadRes.statusText}`);
  }

  // 2. 生成唯一的 multipart 分隔边界
  const boundary = `----CloudflareWorkerBoundary${crypto.randomUUID()}`;

  // 3. 构造表单字段的头部内容(遵循HTTP multipart规范)
  const header = `--${boundary}\r\nContent-Disposition: form-data; name="${fieldName}"; filename="${fileName}"\r\nContent-Type: ${downloadRes.headers.get('Content-Type') || 'application/octet-stream'}\r\n\r\n`;
  // 构造请求体末尾的结束边界
  const footer = `\r\n--${boundary}--\r\n`;

  // 4. 拼接头部、文件流、尾部为一个完整的可读流
  const combinedStream = new ReadableStream({
    async start(controller) {
      // 先写入头部
      controller.enqueue(new TextEncoder().encode(header));
      
      // 逐段读取下载流并转发到控制器
      const reader = downloadRes.body.getReader();
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          controller.enqueue(value);
        }
        // 写入尾部边界并关闭流
        controller.enqueue(new TextEncoder().encode(footer));
        controller.close();
      } catch (err) {
        controller.error(err);
        reader.cancel(err);
      }
    }
  });

  // 5. 发起上传请求,使用拼接后的流作为请求体
  const uploadRes = await fetch(uploadUrl, {
    method: 'POST',
    headers: {
      'Content-Type': `multipart/form-data; boundary=${boundary}`
    },
    body: combinedStream
  });

  if (!uploadRes.ok) {
    throw new Error(`上传失败: ${uploadRes.status} ${uploadRes.statusText}`);
  }

  return uploadRes;
}

// Worker 请求入口示例
addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request: Request) {
  try {
    await streamTransfer(
      'http://someurltoDownload',
      'http://someurltoUpload',
      'file',
      'my-file.jpg'
    );
    return new Response('中转上传成功', { status: 200 });
  } catch (err) {
    return new Response(`错误: ${(err as Error).message}`, { status: 500 });
  }
}

关键细节说明

  1. 零内存占用:整个过程只是逐段转发下载流的内容到上传流,不会把文件全部加载到内存,完全符合 Cloudflare Worker 的内存限制。
  2. 规范兼容:手动构造的 multipart 格式严格遵循 RFC 7578 标准,服务器端可以正常解析。
  3. 类型与错误处理:添加了下载、流转发、上传各环节的错误捕获,同时保留了原文件的 Content-Type 信息。
  4. 边界唯一性:用 crypto.randomUUID() 生成唯一边界,避免和文件内容中的字符串冲突。

额外注意事项

  • 如果需要添加多个表单字段,只需在拼接流时依次加入每个字段的头部、值(或流)、分隔边界即可。
  • 若要处理中文文件名,可将文件名用 encodeURIComponent 编码后放入头部,或遵循 RFC 5987 标准进行编码。
  • 若下载的响应有 Content-Length 头,也可以把它加入到上传请求的头中(部分服务器会依赖这个字段)。

火山引擎 最新活动