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
相关产品推荐
相关产品推荐

