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

Flutter Dio上传图片遇FormData已终结错误,求修复方案

修复Dio刷新Token后上传图片的FormData已Finalized错误

错误Bad state: The FormData has already been finalized的核心原因是:同一个FormData实例只能被Dio处理一次。当拦截器中复用原请求的requestOptions时,其中的FormData已经在第一次请求中被消费,无法再次使用。


解决方案

1. 拦截器中重新构建请求的FormData

在拦截器的错误处理逻辑里,必须重新创建MultipartFile和FormData,替换原请求的data,不能直接复用原requestOptions。

修改后的拦截器代码:

dio.interceptors.addAll([
  PrettyDioLogger(
      requestHeader: true,
      requestBody: true,
      responseBody: true,
      responseHeader: false,
      error: true,
      compact: true,
      request: true,
      maxWidth: 150),

  dios.InterceptorsWrapper(
    onError: (dios.DioException e, handler) async {
      if (e.response?.data['code'] == 'token_not_valid') {
        // 刷新Token
        RefreshTokenResponse? newAccessToken = await refreshToken(
            RefreshTokenBody(
                refresh: storageService.getRefreshToken() ?? ''));

        if (newAccessToken?.access == null) {
          // 刷新失败,直接传递错误
          return handler.next(e);
        }

        // 保存新Token
        storageService.setAccessToken(newAccessToken!.access);

        // 复制原请求配置,避免修改原实例
        final newRequestOptions = e.requestOptions.copyWith();
        // 更新Authorization header
        newRequestOptions.headers['Authorization'] = 'Bearer ${newAccessToken.access}';

        // 重新构建FormData(关键步骤)
        if (e.requestOptions.data is dios.FormData) {
          final originalFormData = e.requestOptions.data as dios.FormData;
          final newFormData = dios.FormData();
          
          // 复制原字段
          for (final entry in originalFormData.fields) {
            newFormData.fields.add(entry);
          }
          // 重新创建MultipartFile
          for (final entry in originalFormData.files) {
            final file = entry.value;
            if (file is dios.MultipartFile && file.path != null) {
              final newMultipartFile = await dios.MultipartFile.fromFile(
                file.path!,
                filename: file.filename,
              );
              newFormData.files.add(MapEntry(entry.key, newMultipartFile));
            }
          }
          newRequestOptions.data = newFormData;
        }

        // 用新配置重新发起请求
        final response = await dio.fetch(newRequestOptions);
        return handler.resolve(response);
      }
      return handler.next(e);
    },
  ),
]);

2. 简化业务代码,移除冗余逻辑

拦截器已经处理了token_not_valid的重试逻辑,业务代码中无需重复判断,同时优化header配置:

final token = storageService.getAccessToken();
String fileName = photo.path.split('/').last;
Map<String, String> headers = <String, String>{
  'Accept': 'application/json',
  'Authorization': 'Bearer $token!', // 统一添加Bearer前缀
};

final formData = dios.FormData.fromMap({
  "image": await dios.MultipartFile.fromFile(
    photo.path,
    filename: fileName,
  ),
});
final response = await dio.patch(
  "${AppConfig.endPoint}api/upload-picture",
  data: formData,
  options: dios.Options(headers: headers),
);

if (response.statusCode == 200) {
  getUserProfile();
  return true;
} else {
  throw Exception(response.statusMessage);
}

关键注意事项

  • 禁止复用FormData实例:每次请求(包括重试)都要创建新的FormData,避免实例被finalized后无法使用。
  • 复制请求配置:使用requestOptions.copyWith()创建新配置,防止修改原请求实例引发未知问题。
  • 自动处理Content-Type:当请求data为FormData时,Dio会自动设置content-type: multipart/form-data,无需手动添加,避免冲突。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 07:24:53