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

如何在formidable文件上传请求中等待writeHandler的执行结果?

问题描述

使用formidable将文件直接上传至AWS S3时,按照官方示例能正常运行,但需要等待S3上传完成后,在HTTP响应中返回文件的最终URL。当前form.parse会立即返回,无法等待S3上传完成;尝试用Promise/await改造uploadStream时,因类型要求必须返回Writable流而非Promise,导致报错:

Type '(file: any) => Promise<any>' is not assignable to type '(file?: VolatileFile | undefined) => Writable'.

现有核心代码:

const s3Client = new AWS.S3({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_KEY,
  },
});

const uploadStream = (file) => {
  const pass = new PassThrough();
  s3Client.upload(
    {
      Bucket: 'demo-bucket',
      Key: file.newFilename,
      Body: pass,
    },
    (err, data) => {
      console.log(err, data);
    },
  );

  return pass;
};

const server = http.createServer((req, res) => {
  if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
    const form = formidable({
      fileWriteStreamHandler: uploadStream,
    });

    form.parse(req, () => {
      res.writeHead(200);
      res.end();
    });

    return;
  }
});

期望实现:在S3上传完成后,返回文件URL:

form.parse(req, () => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ url: 'https://s3.amazonaws.com/my-file' }, null, 2));
});

解决方案

方法一:收集上传Promise,等待全部完成后响应

核心思路是在uploadStream内部为每个文件创建上传Promise,收集所有Promise后,在form.parse的回调中等待全部完成,再提取URL返回。

改造后代码:

const { PassThrough } = require('stream');
const AWS = require('aws-sdk');
const http = require('http');
const formidable = require('formidable');

const s3Client = new AWS.S3({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_KEY,
  },
});

// 存储所有文件的上传Promise
let uploadPromises = [];

const uploadStream = (file) => {
  const pass = new PassThrough();
  // 创建S3上传Promise
  const uploadPromise = new Promise((resolve, reject) => {
    s3Client.upload(
      {
        Bucket: 'demo-bucket',
        Key: file.newFilename,
        Body: pass,
      },
      (err, data) => {
        if (err) reject(err);
        else resolve(data);
      },
    );
  });
  // 将Promise加入数组
  uploadPromises.push(uploadPromise);
  return pass;
};

const server = http.createServer((req, res) => {
  if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
    uploadPromises = []; // 重置Promise数组,避免跨请求污染
    const form = formidable({
      fileWriteStreamHandler: uploadStream,
    });

    form.parse(req, async (err, fields, files) => {
      if (err) {
        res.writeHead(500);
        res.end(JSON.stringify({ error: err.message }));
        return;
      }
      try {
        // 等待所有文件上传到S3完成
        const uploadResults = await Promise.all(uploadPromises);
        // 提取第一个文件的URL(多文件可遍历处理)
        const fileUrl = uploadResults[0].Location;
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ url: fileUrl }, null, 2));
      } catch (uploadErr) {
        res.writeHead(500);
        res.end(JSON.stringify({ error: uploadErr.message }));
      }
    });

    return;
  }
  // 处理其他请求
  res.writeHead(404);
  res.end('Not Found');
});

方法二:监听formidable的file事件处理上传

不使用fileWriteStreamHandler,而是直接监听form的file事件,在事件回调中处理单个文件的流上传,并收集Promise,最后在end事件中等待全部完成。

代码示例:

const { PassThrough } = require('stream');
const AWS = require('aws-sdk');
const http = require('http');
const formidable = require('formidable');

const s3Client = new AWS.S3({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_KEY,
  },
});

const server = http.createServer((req, res) => {
  if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
    const uploadPromises = [];
    const form = formidable();

    // 监听每个文件的上传事件
    form.on('file', (name, file) => {
      const pass = new PassThrough();
      // 创建上传Promise
      const uploadPromise = new Promise((resolve, reject) => {
        s3Client.upload(
          {
            Bucket: 'demo-bucket',
            Key: file.newFilename,
            Body: pass,
          },
          (err, data) => {
            if (err) reject(err);
            else resolve(data);
          },
        );
      });
      uploadPromises.push(uploadPromise);
      // 将文件流导向PassThrough,传给S3
      file.stream.pipe(pass);
    });

    // 所有文件解析完成后,等待S3上传完成
    form.on('end', async () => {
      try {
        const uploadResults = await Promise.all(uploadPromises);
        const fileUrl = uploadResults[0].Location;
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ url: fileUrl }, null, 2));
      } catch (err) {
        res.writeHead(500);
        res.end(JSON.stringify({ error: err.message }));
      }
    });

    // 处理解析错误
    form.on('error', (err) => {
      res.writeHead(500);
      res.end(JSON.stringify({ error: err.message }));
    });

    form.parse(req);
    return;
  }
  res.writeHead(404);
  res.end('Not Found');
});

关键说明

  • fileWriteStreamHandler强制要求返回Writable流,因此不能直接返回Promise,但可以在函数内部创建Promise并收集
  • 使用Promise.all等待所有文件的S3上传完成,S3的upload回调返回的data.Location就是文件的公开URL
  • 必须处理错误场景:包括form解析错误、S3上传错误,避免请求挂起或无响应

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 08:23:26