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

React向Asp.Net Core后端多文件(图片)上传失败求助

React多图片上传至ASP.NET Core后端400错误解决方法

问题描述

React前端多图片上传至ASP.NET Core后端时出现400错误,后端无法识别图片集合;但单文件上传功能正常。前端通过Blob格式发送图片,后端使用MediatR处理请求。

相关代码

后端代码

Command实体类

public class Command : IRequest<ProductDTO>
{
    public string Title { get; set; }
    public string Description { get; set; }
    public double Price { get; set; }
    public IEnumerable<IFormFile> Photos { get; set; } // 多文件接收属性(原问题中无法工作)
    // public IFormFile Photo { get; set; } // 单文件接收属性(可正常工作)
}

控制器代码

[HttpPost]
public async Task<ActionResult<ProductDTO>> CreateProduct([FromForm] CreateProduct.Command command)
{
    return await _mediator.Send(command);
}

前端代码

IPhoto接口

export interface IPhoto {
  lastModified: number;
  lastModifiedDate: Date;
  name: string;
  path: string;
  preview: string;
  size: number;
  type: string;
  webkitRelativePath: string;
}

DropZone组件

import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';

import { IPhoto } from '../../../models/Photo';

const dropZoneStyles = {
  border: 'dashed 3px',
  borderColor: '#eee',
  borderRadius: '5px',
  paddingTop: '30px',
  textAlign: 'center' as const,
  height: '200px',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  padding: '0',
};

const dropZoneActive = {
  borderColor: 'green',
};

interface Props {
  setFiles: (files: IPhoto[]) => void;
}

const Dropzone = ({ setFiles }: Props): JSX.Element => {
  const onDrop = useCallback(
    (files: any) => {
      setFiles(
        files.map((file: any) => Object.assign(file, { preview: URL.createObjectURL(file) }))
      );
    },
    [setFiles]
  );

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    multiple: true,
    accept: {
      'image/jpeg': [],
      'image/png': [],
    },
  });

  return (
    <div
      {...getRootProps()}
      style={
        isDragActive
          ? {
              ...dropZoneStyles,
              ...dropZoneActive,
            }
          : dropZoneStyles
      }
    >
      <input {...getInputProps()} />
      <h4>Drop an Image here</h4>
    </div>
  );
};

export default Dropzone;

原提交逻辑(错误版本)

const [files, setFiles] = useState<any[]>([]);

const onSubmit = async (data: Omit<ProductValues, 'photos'>) => {
  const photos = files.map((file) => new Blob([file], { type: 'image / png' })); // 错误:将File转为Blob,且未正确构造FormData
  // const photo = new Blob([files[0]], { type: 'image / png' }); // 单文件时可工作

  dispatch(
    productCreateProductActions.request({
      product: {
        ...data,
        photos: photos,
      },
    })
  );
};

原API请求代码(错误版本)

import { apiClient, ExtendedAxiosResponse } from '../../helpers/api-client';
import { Product, ProductValues } from '../../models/Product';

export const api = {
  getProducts: (): Promise<ExtendedAxiosResponse> => apiClient.get('/products'),
  createProduct: (product: ProductValues): Promise<ExtendedAxiosResponse> =>
    apiClient.post(
      '/products',
      {
        ...product,
      },
      {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      }
    ),
  deleteProduct: (id: string): Promise<ExtendedAxiosResponse> =>
    apiClient.delete(`/products/${id}`),
};

错误原因

  1. FormData构造错误:直接将包含文件数组的普通对象传给axios,即使设置了multipart/form-data,axios无法自动将数组转换为符合多文件上传要求的FormData格式,后端无法解析。
  2. 多余的Blob转换:Dropzone获取的file本身就是File对象(Blob的子类),手动转为Blob完全多余,还可能破坏文件结构。
  3. Content-Type设置错误:手动设置multipart/form-data时,axios不会自动生成boundary分隔符,导致后端无法正确解析请求体。

解决方案

1. 修改前端提交逻辑,手动构造FormData

const [files, setFiles] = useState<File[]>([]); // 直接使用File类型,如需预览可保留扩展属性

const onSubmit = async (data: Omit<ProductValues, 'photos'>) => {
  const formData = new FormData();
  // 添加普通文本字段
  formData.append('Title', data.Title);
  formData.append('Description', data.Description);
  formData.append('Price', data.Price.toString());
  // 添加多文件,键名必须与后端Command的`Photos`属性完全一致
  files.forEach(file => {
    formData.append('Photos', file);
  });

  dispatch(
    productCreateProductActions.request({
      product: formData, // 传递FormData而非普通对象
    })
  );
};

2. 修改API请求代码,移除手动Content-Type设置

export const api = {
  // 其他接口不变
  createProduct: (formData: FormData): Promise<ExtendedAxiosResponse> =>
    apiClient.post('/products', formData) // 让axios自动处理Content-Type,会自动添加正确的boundary
};

3. 调整Saga中的参数传递

function* createProduct({ payload }: AppAction<CreateProductActions>) {
  const resp: ExtendedAxiosResponse = yield call(api.createProduct, payload.product);

  // 后续逻辑保持不变
}

4. 后端代码无需修改

后端的Command类和控制器代码已经正确配置,[FromForm]会自动解析FormData中的多文件到IEnumerable<IFormFile> Photos属性。

验证说明

修改后,前端发送的请求体将以正确的multipart/form-data格式传递,多文件会以相同的Photos键名多次出现,后端能正确绑定到IEnumerable<IFormFile>集合,解决400错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 12:48:19