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

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

问题根源分析

  1. 前端请求格式错误:将FormData嵌套在dataA字段中以JSON对象形式发送,而含文件的请求必须用multipart/form-data格式,JSON无法正确传递文件;同时variationData直接作为数组放入JSON,后端无法解析混合格式的请求。
  2. 后端配置冲突:同时使用全局multer中间件和NestJS的FileInterceptor,导致文件解析逻辑冲突,无法正确捕获上传的图片。
  3. DTO结构不匹配:CreateProductDto定义的productData和variationData结构与前端实际发送的数据不符,导致解析后的数据为空或错误。
  4. 服务层未做空值校验:直接访问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会因找不到目录报错。

验证结果

  1. 前端提交表单后,后端成功接收图片和表单数据,图片存储到uploads目录。
  2. 数据库中Product实体的image字段保存生成的文件名(如1689319709094-123456789.jpeg)。
  3. 前端可通过http://localhost:3000/uploads/[生成的文件名]访问上传的图片。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 23:43:09