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

Flutter使用Dio上传多图到FastAPI时返回422 Unprocessable Entity错误

Flutter上传多图+表单到FastAPI报422错误修复

问题表现

  • 接口在Swagger UI(localhost:8000/docs)、Postman、curl调用下全部正常,仅Flutter端调用返回422 Unprocessable Entity
  • 报错明确提示两个校验失败:
    • price字段值不是合法float类型
    • files字段为必填项但缺失
  • 已尝试手动添加multipart请求头、固定Content-Type,问题仍存在

相关代码

原Flutter端上传逻辑(存在问题)

_uploadImage() async {
  for (int i = 0; i < imageFileList!.length; i++) {
    var path = imageFileList![i].path;
    _images!.add(await MultipartFile.fromFile(path,
        filename: path.split('/').last,
        contentType: MediaType("image", "jpg")));
    var formData = FormData.fromMap(
      {
        "name": productNameController,
        "price": productPriceController,
        "description": productDescriptionController,
        "image": _images,
      },
    );
    var response = await DioClient.dio.post(
        "http://10.0.2.2:8000/products/addProductFD",
        data: formData,
        options: Options(contentType: 'multipart/form-data'));
    debugPrint(response.toString());
  }
}

FastAPI后端接口定义

@router.post('/addProductFD', status_code=status.HTTP_201_CREATED)
async def create(
    name: str = Form(...),
    price: float = Form(...),
    description: str = Form(...),
    files: List[UploadFile] = File(...),
    db: Session = Depends(get_db),
):
    fileList = []
    for file in files:
        try:
            FILEPATH = "./static/product_images/"
            pimage_name = FILEPATH + imghex(file.filename)
            contents = await file.read()
            with open(pimage_name, 'wb') as f:
                f.write(contents)
        except Exception:
            return {"message": "There was an error uploading the file(s)"}
        finally:
            await file.close()
        fileList.append("localhost:8000" + pimage_name[1:])

    file_urls = ",".join(fileList)
    new_item = Product(
        name=name,
        price=price,
        description=description,
        imgs_url=[file_urls],
    )

    db.add(new_item)
    db.commit()
    db.refresh(new_item)
    return new_item

正常调用参考

curl调用示例:

curl -X 'POST' \
  'http://127.0.0.1:8000/products/addProductFD' \
  -H 'accept: application/json' \
  -H 'Content-Type: multipart/form-data' \
  -F 'name=iPhone 44' \
  -F 'price=1800' \
  -F 'description=Brand New with a fresh box.' \
  -F 'files=@iphone1.jpg;type=image/jpeg' \
  -F 'files=@iphone1ss.jpg;type=image/jpeg'

接口正常返回:

{
  "name": "iPhone 44",
  "price": 1800,
  "is_active": true,
  "imgs_url": [
    "localhost:8000/static/product_images/0337698056d37da14fae.jpg,localhost:8000/static/product_images/db2f6998aa87a611c89e.jpg"
  ],
  "id": 4,
  "description": "Brand New with a fresh box.",
  "owner_id": null
}

根因定位

代码里一共4个直接触发报错的问题:

  1. 表单字段传值错误:直接把TextEditingController控制器对象作为字段值传给后端,没有取控制器存储的实际输入文本(需要用.text属性获取),price字段也没有做类型转换,后端收到的是控制器实例的字符串,自然无法解析为float类型。
  2. 字段名不匹配:后端定义的文件接收字段名是files,Flutter端写的字段名是image,后端找不到对应字段直接报缺失。
  3. 请求逻辑位置错误:把构造FormData、发送请求的逻辑写在了遍历图片的for循环内部,选N张图就会重复发N次请求,且前N-1次请求携带的图片列表都是不完整的。
  4. 冗余请求头导致解析失败:Dio发送FormData时会自动生成带boundary分隔符的multipart/form-data请求头,手动固定写死contentType: 'multipart/form-data'会丢失关键的boundary参数,直接导致后端无法解析表单内容。

修复方案

把原_uploadImage方法替换为以下实现即可:

_uploadImage() async {
  // 清空历史缓存,避免重复提交之前选的图
  _images!.clear();
  // 先遍历处理完全部选中的图片,不要在循环里发请求
  for (int i = 0; i < imageFileList!.length; i++) {
    var path = imageFileList![i].path;
    // 按实际文件后缀匹配媒体类型,不要硬编码jpg
    String fileExt = path.split('.').last.toLowerCase();
    _images!.add(await MultipartFile.fromFile(
      path,
      filename: path.split('/').last,
      contentType: MediaType("image", fileExt == 'png' ? 'png' : 'jpeg')
    ));
  }
  // 所有文件处理完成后一次性构造表单
  var formData = FormData.fromMap(
    {
      // 取控制器的text属性获取实际输入值,price转为double类型
      "name": productNameController.text.trim(),
      "price": double.parse(productPriceController.text.trim()),
      "description": productDescriptionController.text.trim(),
      // 字段名和后端保持一致,用files
      "files": _images,
    },
  );
  // 发送请求,不要手动指定contentType,让Dio自动生成带boundary的合法请求头
  var response = await DioClient.dio.post(
    "http://10.0.2.2:8000/products/addProductFD",
    data: formData
  );
  debugPrint(response.toString());
}

额外优化建议

  • 提交前加表单校验:判断名称、价格、描述是否为空,价格输入是否为合法数字,是否选中了至少1张图片,避免运行时报错。
  • 如果项目里配置了全局Dio拦截器,确认拦截器没有对FormData类型的请求做额外的JSON序列化处理,避免破坏表单结构。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 23:03:23