Multer批量上传偶发失败无报错,异步逻辑问题排查求助
批量文件上传至数据库偶发异常问题排查与修复
问题描述
我开发了一个使用Multer中间件批量将文件存储到数据库的应用,整体配置完成且多数场景运行正常,但偶尔出现上传异常:文件已上传至服务器,但未保存到数据库,且无任何错误提示。怀疑是数据库连接延迟等问题,尝试将POST请求改为异步处理,但不确定异步逻辑是否正确。期望实现所有上传请求成功执行,或出现问题时生成明确的错误提示。
前端代码
import React, { useState } from 'react' import BackButton from '../components/BackButton'; import Spinner from '../components/Spinner'; import axios from 'axios'; import { useNavigate } from 'react-router-dom'; export const CreatePhoto = () => { const [category, setCategory] = useState('Reception'); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const handleChange = (e) => { setCategory(e.target.value); } const handlePhoto = (e) => { setFiles(Array.from(e.target.files)) } const handleSubmit = (e) => { e.preventDefault(); // 阻止表单默认提交行为 if (!files.length) { alert('请选择要上传的图片'); return; } const formData = new FormData(); formData.append('category', category); files.forEach(file => { formData.append('photo', file); }); setLoading(true); axios .post('http://localhost:5555/uploadImage', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) .then(() => { setLoading(false); alert('上传成功'); navigate('/'); }) .catch((error) => { setLoading(false); alert(`上传失败:${error.response?.data?.error || '未知错误'}`); console.error(error); }); }; return ( <div className='p-4'> <BackButton /> <h1 className='text-3xl my-4'>Upload Photos</h1> {loading ? <Spinner /> : ''} <div className='flex flex-col border-2 border-sky-400 rounded-xl w-[600px] p-4 mx-auto'> <form onSubmit={handleSubmit} encType='multipart/form-data'> <div className='my-4'> <label className='mb-2 inline-block text-neutral-500 dark:text-gray-800' htmlFor='category'>Category</label> <select id="category" name="category" className='bg-gray-50 border-2 border-yellow-600 text-gray-900 text-base rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-100 dark:border-yellow-500 dark:placeholder-gray-400 dark:text-gray-800 dark:focus:ring-blue-500 dark:focus:border-blue-500' value={category} onChange={handleChange}> <option value="Reception">Choose a Category</option> <option value="Traditional">Traditional</option> <option value="Court">Court</option> <option value="Reception">Reception</option> </select> </div> <div className='mb-3'> <label htmlFor="formFileMultiple" className='mb-2 inline-block text-neutral-500 dark:text-gray-800' >Upload Images</label> <input className='relative m-0 block w-full min-w-0 flex-auto cursor-pointer rounded border border-2 border-yellow-500 bg-gray-100 bg-clip-padding px-3 py-[0.32rem] text-base font-normal text-surface transition duration-300 ease-in-out file:-mx-3 file:-my-[0.32rem] file:me-3 file:cursor-pointer file:overflow-hidden file:rounded-none file:border-0 file:border-e file:border-solid file:border-inherit file:bg-transparent file:px-3 file:py-[0.32rem] file:text-surface focus:border-primary focus:text-gray-700 focus:shadow-inset focus:outline-none dark:border-yellow dark:text-black file:dark:text-black' type="file" name="photo" onChange={handlePhoto} id="formFileMultiple" multiple="multiple" /> </div> <button className='p-2 bg-sky-300 m-8' type='submit'> Upload </button> </form> </div> </div> ) } export default CreatePhoto
后端代码(原代码)
// ... 省略其他配置 const fileStorage = multer.diskStorage({ destination: 'images', filename : (request, file, cb) => { cb(null, file.fieldname + '_' + Date.now() + path.extname(file.originalname)); } }); const uploadImage = multer({ storage : fileStorage, limits : { fileSize: 10000000 }, fileFilter(request, file, cb) { if (!file.originalname.match(/\.(png|jpg|jpeg)$/)) { return cb(new Error('Please upload an Image file!')); } cb(undefined, true); } }); app.post('/uploadImage', uploadImage.array('photo', 20), async (request, response) => { const files = request.files; console.log(request.body); console.log(request.files); const imgArray = await files.map((file) => { const img = fs.readFileSync(file.path); return img.toString('base64'); }); imgArray.map(async (src, index) => { const finalImg = { title : files[index].originalname, path : files[index].path, fileSize : files[index].size, category : request.body.category } console.log(); const newUpload = new Photo(finalImg); try { await newUpload.save(); console.log("Upload was good"); response.json('Upload was successful!!'); } catch (error) { response.status(400).json({error:error.message || `Something went wrong somewhere`}); } }); });
问题根源分析
- 异步逻辑错误:使用
imgArray.map(async (...))时,map不会等待异步函数执行完成,导致服务器可能在所有数据库保存操作完成前就结束请求,部分文件保存失败无法被捕获。 - 重复响应问题:循环中每次调用
response.json()或response.status().json(),会尝试多次向客户端发送响应,Node.js会阻止后续响应,导致部分错误无法传递到前端。 - 无效的await使用:
await files.map(...)无效,因为map返回的是普通数组而非Promise数组,无法等待所有文件读取完成。
修复后的后端代码
// ... 省略其他配置 const fs = require('fs').promises; // 使用fs的promise版本,避免同步阻塞 const fileStorage = multer.diskStorage({ destination: 'images', filename : (request, file, cb) => { cb(null, file.fieldname + '_' + Date.now() + path.extname(file.originalname)); } }); const uploadImage = multer({ storage : fileStorage, limits : { fileSize: 10000000 }, fileFilter(request, file, cb) { if (!file.originalname.match(/\.(png|jpg|jpeg)$/)) { return cb(new Error('Please upload an Image file!')); } cb(undefined, true); } }); app.post('/uploadImage', uploadImage.array('photo', 20), async (request, response) => { try { const files = request.files; const category = request.body.category; // 用Promise.all等待所有文件处理完成 const savePromises = files.map(async (file) => { // 异步读取文件,避免阻塞事件循环 // const img = await fs.readFile(file.path); // const base64Img = img.toString('base64'); // 注:如果数据库不需要base64内容,可以省略读取步骤,直接存文件信息 const finalImg = { title: file.originalname, path: file.path, fileSize: file.size, category: category }; const newUpload = new Photo(finalImg); return newUpload.save(); }); // 等待所有保存操作完成 await Promise.all(savePromises); console.log('所有文件上传成功'); response.json('所有文件上传成功!!'); } catch (error) { console.error('上传失败:', error); response.status(400).json({ error: error.message || '文件保存到数据库失败' }); } });
关键修复点说明
- 统一异步处理:使用
Promise.all等待所有文件的数据库保存操作完成,确保所有文件都处理完毕后再响应客户端。 - 避免重复响应:将响应逻辑移到try/catch块的最外层,只发送一次成功或失败响应。
- 异步文件读取:使用
fs.promises.readFile替代同步的fs.readFileSync,避免阻塞Node.js事件循环,提升并发处理能力。 - 前端优化:修复了表单默认提交行为阻止、文件状态初始化、错误提示优化等问题,提升用户体验。
内容的提问来源于stack exchange,提问作者Teke Bethelson
相关产品推荐
相关产品推荐

