Express Multer中间件data事件异常及文件上传进度获取问题
问题描述
需求为获取文件上传过程中的实时请求进度,最初采用如下写法:
app.post('/upload', upload.single('file'), (req,res,next) => { req.on('data', ()=> { // 处理进度逻辑 }) // 处理上传后的文件 })
本地及AWS EC2环境下均无法实现需求:处理进度逻辑仅在Multer中间件接收完所有文件后执行。
调整写法后:
app.post('/upload', (req,res,next) => { req.on('data', ()=> { // 处理进度逻辑 }) next() }, upload.single('file'), (req, res, next) => { // 处理上传后的文件 })
- 本地环境:正常工作,每收到一段文件分片就触发进度逻辑;
- AWS EC2环境:进度逻辑仍在文件接收完成后批量触发,无法实时监听。
补充:曾尝试在data事件内上传完成后调用next(),但Multer抛出异常:错误:表单在Multipart._final阶段意外结束(原英文:Error: Unexpected end of form at Multipart._final)。
差异原因
本地环境无代理层,请求数据会直接流式传输到后端服务,因此req.data事件会随分片数据到达实时触发;而AWS EC2通常搭配**应用负载均衡(ALB)**使用,ALB默认会对multipart/form-data类型的请求进行完整缓冲,即等待整个请求接收完毕后再转发给后端服务,导致后端的req.data事件一次性触发,无法实时获取上传进度。
解决方案
方案1:关闭AWS ALB的请求缓冲
可在ALB的目标组配置中关闭“请求缓冲”功能,让请求数据直接流式转发给EC2实例。但此操作可能会增加ALB的性能开销,需根据业务场景权衡。
方案2:基于Multer底层的Busboy监听进度
Multer基于Busboy实现,直接使用Busboy处理请求,可绕过代理缓冲对req.data事件的影响,实时监听文件分片上传:
const Busboy = require('busboy'); app.post('/upload', (req, res) => { const busboy = new Busboy({ headers: req.headers }); let totalFileSize = 0; let uploadedBytes = 0; // 监听文件字段 busboy.on('file', (fieldname, fileStream, filename, encoding, mimetype) => { // 获取文件总大小 totalFileSize = fileStream.headers['content-length']; // 监听分片数据 fileStream.on('data', (chunk) => { uploadedBytes += chunk.length; // 实时计算并处理进度,例如返回给前端 const progress = ((uploadedBytes / totalFileSize) * 100).toFixed(2); console.log(`当前上传进度:${progress}%`); }); // 文件上传完成回调 fileStream.on('end', () => { console.log(`${filename} 上传完成`); }); }); // 监听表单字段 busboy.on('field', (fieldname, val) => { console.log(`表单字段 ${fieldname}: ${val}`); }); // 整个表单处理完成 busboy.on('finish', () => { res.status(200).send('上传完成'); }); // 将请求流导入Busboy req.pipe(busboy); });
方案3:自定义Multer存储引擎监听进度
通过自定义Multer的存储引擎,在文件写入过程中监听数据流的data事件:
const multer = require('multer'); const fs = require('fs'); const path = require('path'); const customStorage = multer.diskStorage({ destination: (req, file, cb) => { const uploadDir = './uploads'; // 确保上传目录存在 if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir); cb(null, uploadDir); }, filename: (req, file, cb) => { // 自定义文件名 cb(null, `${Date.now()}-${file.originalname}`); }, // 重写_write方法监听进度 _write: function (file, encoding, cb) { const filePath = path.join(this.destination, this.filename); const writeStream = fs.createWriteStream(filePath); let uploadedBytes = 0; file.pipe(writeStream); // 监听分片数据 file.on('data', (chunk) => { uploadedBytes += chunk.length; const progress = ((uploadedBytes / file.size) * 100).toFixed(2); console.log(`上传进度:${progress}%`); }); // 写入完成回调 writeStream.on('finish', cb); } }); const upload = multer({ storage: customStorage }); app.post('/upload', upload.single('file'), (req, res) => { res.status(200).send({ message: '文件上传完成', file: req.file }); });
异常说明
之前在data事件内调用next()导致的错误:表单在Multipart._final阶段意外结束,是因为提前调用next()会中断请求流的传输,Multer还未完成整个表单的解析就收到了流结束信号,从而抛出异常。
内容的提问来源于stack exchange,提问作者Laurent Schwitter

