React表单上传至NestJs出现500错误及文件读取未定义问题
React表单上传至NestJS后端的问题解决(含图片存储与展示)
问题概述
React上传包含图片的表单数据到NestJS后端时,后端抛出TypeError: Cannot read properties of undefined (reading 'filename'),前端返回500 Internal Server Error。控制器打印显示createProductDto的dataA为空对象,image参数为undefined。需实现图片在后端正确存储,且前端可正常获取展示。
错误信息
后端错误
TypeError: Cannot read properties of undefined (reading 'filename') at ProductService.createProduct (E:\QUEPER\Nest\admin_portal_backend\src\product\product.service.ts:23:29)
前端错误
xhr.js:251 POST http://localhost:3000/products 500 (Internal Server Error) dispatchXhrRequest @ xhr.js:251 xhr @ xhr.js:49 dispatchRequest @ dispatchRequest.js:51 request @ Axios.js:148 httpMethod @ Axios.js:187 wrap @ bind.js:5 saveProduct @ admin_page.js:28 handleConfirm @ admin_page.js:48 onSubmit @ admin_page.js:173 callCallback @ react-dom.development.js:4164 invokeGuardedCallbackDev @ react-dom.development.js:4213 invokeGuardedCallback @ react-dom.development.js:4277 invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:4291 executeDispatch @ react-dom.development.js:9041 processDispatchQueueItemsInOrder @ react-dom.development.js:9073 processDispatchQueue @ react-dom.development.js:9086 dispatchEventsForPlugins @ react-dom.development.js:9097 (anonymous) @ react-dom.development.js:9288 batchedUpdates$1 @ react-dom.development.js:26140 batchedUpdates @ react-dom.development.js:3991 dispatchEventForPluginEventSystem @ react-dom.development.js:9287 dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ react-dom.development.js:6465 dispatchEvent @ react-dom.development.js:6457 dispatchDiscreteEvent @ react-dom.development.js:6430 admin_page.js:31 AxiosError {message: 'Request failed with status code 500', name: 'AxiosError', code: 'ERR_BAD_RESPONSE', config: {…}, request: XMLHttpRequest, …}
控制器打印信息
{ dataA: {}, dataB: [ { color: 'c1', size: 's1' } ] } undefined
问题根源分析
- 前端请求格式错误:将
FormData嵌套在dataA字段中以JSON对象形式发送,而含文件的请求必须用multipart/form-data格式,JSON无法正确传递文件;同时variationData直接作为数组放入JSON,后端无法解析混合格式的请求。 - 后端配置冲突:同时使用全局
multer中间件和NestJS的FileInterceptor,导致文件解析逻辑冲突,无法正确捕获上传的图片。 - DTO结构不匹配:
CreateProductDto定义的productData和variationData结构与前端实际发送的数据不符,导致解析后的数据为空或错误。 - 服务层未做空值校验:直接访问
image.filename,未判断image是否存在,触发空指针错误。
修复步骤
前端代码修复
将所有数据(包括图片和variationData)放入FormData中,以multipart/form-data格式发送请求:
const saveProduct = async (data) => { try { const formData = new FormData(); // 添加图片文件 formData.append('image', data.productData.image); // 添加产品基本信息 formData.append('productName', data.productData.productName); formData.append('desc', data.productData.desc); formData.append('regularPrice', data.productData.regularPrice); formData.append('salePrice', data.productData.salePrice); // 将variationData转为JSON字符串后添加 formData.append('variationData', JSON.stringify(data.variationData)); const response = await axios.post('http://localhost:3000/products', formData); return response.data; } catch (error) { console.error(error); } };
后端代码修复
1. 移除全局multer中间件(main.ts)
删除全局multer配置,避免与控制器的FileInterceptor冲突:
async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); // Enable CORS app.enableCors({ origin: 'http://localhost:3001', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', credentials: true, }); // Serve uploaded files as static files const uploadsPath = path.join(__dirname, 'uploads'); app.useStaticAssets(uploadsPath, { prefix: '/uploads/', }); await app.listen(3000); }
2. 调整CreateProductDto结构
修改create-product.dto.ts以匹配前端发送的字段:
export class CreateProductDto { productName: string; desc: string; regularPrice: string; salePrice: string; variationData: string; // 接收JSON字符串形式的规格数据 }
3. 控制器处理数据(product.controller.ts)
配置FileInterceptor并解析variationData的JSON字符串:
import { Controller, Post, Body, UploadedFile, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { diskStorage } from 'multer'; import { extname } from 'path'; import { ProductService } from './product.service'; import { CreateProductDto } from './create-product.dto'; @Controller('products') export class ProductController { constructor(private readonly productService: ProductService) {} @Post() @UseInterceptors(FileInterceptor('image', { storage: diskStorage({ destination: './uploads', filename: (req, file, cb) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const extension = extname(file.originalname); cb(null, `${uniqueSuffix}${extension}`); } }) })) async createProduct( @Body() createProductDto: CreateProductDto, @UploadedFile() image: Express.Multer.File ) { try { // 解析variationData为数组 const variationData = JSON.parse(createProductDto.variationData); const product = await this.productService.createProduct({ ...createProductDto, variationData }, image); return product; } catch (error) { console.error(error); throw error; } } }
4. 服务层添加空值校验并调整逻辑(product.service.ts)
修改服务层代码,添加image空值校验,调整数据处理逻辑:
async createProduct(createProductDto: any, image: Express.Multer.File): Promise<Product> { try { if (!image) { throw new Error('图片文件不能为空'); } // 组装productData const productData = { productName: createProductDto.productName, desc: createProductDto.desc, regularPrice: createProductDto.regularPrice, salePrice: createProductDto.salePrice, image: image.filename // 存储生成的文件名 }; const product = this.productRepository.create(productData); const createdProduct = await this.productRepository.save(product); const filteredVariationData = createProductDto.variationData.filter( variation => variation.color !== '' || variation.size !== '' ); if (filteredVariationData.length > 0) { const createdVariations = filteredVariationData.map(variation => { return this.variationRepository.create({ ...variation, product: createdProduct }); }); await this.variationRepository.save(createdVariations); createdProduct.variations = createdVariations; } return createdProduct; } catch (error) { console.error(error); throw error; } }
5. 确保uploads目录存在
在项目根目录手动创建uploads文件夹,否则multer会因找不到目录报错。
验证结果
- 前端提交表单后,后端成功接收图片和表单数据,图片存储到
uploads目录。 - 数据库中
Product实体的image字段保存生成的文件名(如1689319709094-123456789.jpeg)。 - 前端可通过
http://localhost:3000/uploads/[生成的文件名]访问上传的图片。
内容的提问来源于stack exchange,提问作者Zuha Sohail
相关产品推荐
相关产品推荐

