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

Flutter Dio发起Multipart请求遇400,服务端提示缺file参数

问题:Dio上传Multipart文件返回400,服务端提示缺少request part 'file'

现象

使用Flutter Dio发起头像上传请求时,始终返回HTTP 400状态码,服务端抛出异常:

org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present

但通过Postman和C#代码可以正常发起该上传请求,尝试修改服务端@RequestPart为@RequestParam、手动设置Content-Type: multipart/form-data均无效。

前端核心代码

头像上传逻辑

Future<void> _updateAvatar(AssetEntity entity) async {
  var file = await entity.file;
  if (file == null){
    ToastUtils.showShortError("image not found");
    return;
  }
  var cropped = await ImageCropper().cropImage(
    sourcePath: file.path,
    aspectRatioPresets: [
      CropAspectRatioPreset.square,
    ],
  );
  if (cropped == null){
    ToastUtils.showInfo("crop canceled");
    return;
  }
  var multipartFile = await ImageUtils.multipartFileFromFile(cropped);
  if (multipartFile == null){
    ToastUtils.showShortError("cannot create image file");
    return;
  }
  await UserService.updateAvatar(multipartFile, (p) {
    ToastUtils.showSuccessful("avatar is updated~");
  });
}

生成MultipartFile工具方法

static Future<http.MultipartFile?> multipartFileFromFile(File? file) async {
  if (file == null) {
    return null;
  }
  String uri = file.path;
  int dotIndex =  uri.lastIndexOf(".");
  String filename = DateTime.now().toString() + uri.substring(dotIndex);
  http.MultipartFile mf = await http.MultipartFile.fromPath("file", file.path, filename: filename);
  return mf;
}

封装的上传请求

static Future<void> updateAvatar(http.MultipartFile file, Function(String) responseHandler) async {
  dio.FormData files = dio.FormData.fromMap({
    'file': file,
  });
  await HttpUtils.uploadWithDefaultClient<String?>('/service/user/current/avatar/update', files, null, (data){
    if (data == null){
      return;
    }
    String avatar = data;
    Global.user!.avatar = avatar;
    for (Function listener in avatarListeners) {
      listener(avatar);
    }
    responseHandler(avatar);
  });
}

Future<custom.Response<Map<String, dynamic>?>> postMultipart(String url, FormData formData, [Function(int, int)? onProgress]) async {
  var dio = Dio();
  if (host != null){
    dio.options.baseUrl = host!;
  }
  dio.options.headers = {
    ...headers,
    'Cookie': 'worldstreet_at=$accessToken',
  };
  return await _handleDioRequest(dio.post(url, data: formData, onSendProgress: onProgress));
}

服务端核心代码

@RequestMapping("/current/avatar/update")
public String updateCurrentUserAvatar(@RequestPart("file") MultipartFile file){
    Long uid = LoginUtils.getUid();
    User user = userService.selectByUid(uid);
    FileInfo fileInfo = fileService.uploadAvatar(file);
    ExceptionUtils.failIf(fileInfo == null, "upload failed");
    String avatar = fileInfo.getUrl();
    if (avatar != null){
        fileService.deleteFile(user.getAvatar());
        user.setAvatar(avatar);
    }
    userService.update(user);
    return avatar;
}

问题根源与解决方案

核心问题

代码中混用了http.MultipartFile(dart:http包)和dio.FormData,Dio无法正确解析dart:http的MultipartFile对象,导致请求中没有正确携带名为file的表单部分。

修复方案

修改multipartFileFromFile方法,直接生成Dio的MultipartFile:

static Future<dio.MultipartFile?> multipartFileFromFile(File? file) async {
  if (file == null) {
    return null;
  }
  String uri = file.path;
  int dotIndex = uri.lastIndexOf(".");
  String filename = DateTime.now().toString() + uri.substring(dotIndex);
  
  // 使用Dio自带的MultipartFile.fromPath
  return await dio.MultipartFile.fromPath(
    "file", 
    file.path, 
    filename: filename
  );
}

同时更新updateAvatar方法的参数类型:

// 参数类型从http.MultipartFile改为dio.MultipartFile
static Future<void> updateAvatar(dio.MultipartFile file, Function(String) responseHandler) async {
  dio.FormData files = dio.FormData.fromMap({
    'file': file,
  });
  await HttpUtils.uploadWithDefaultClient<String?>('/service/user/current/avatar/update', files, null, (data){
    if (data == null){
      return;
    }
    String avatar = data;
    Global.user!.avatar = avatar;
    for (Function listener in avatarListeners) {
      listener(avatar);
    }
    responseHandler(avatar);
  });
}

额外验证点

  • 检查Dio拦截器:确保没有拦截器篡改请求头或表单数据,破坏Multipart结构
  • 不要手动设置Content-Type: multipart/form-data:Dio使用FormData时会自动生成带boundary的正确头,手动设置会覆盖boundary导致服务端解析失败

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 17:15:22