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

Flutter Dio拦截器刷新Token后POST文件上传请求重试失败

问题分析与解决方案

核心原因

1. FormData的一次性资源特性

上传文件使用的FormData中,MultipartFile是基于字节流的一次性资源——第一次请求发送时,字节流已被完全读取耗尽。重试时直接复用原FormData实例,会因无有效数据可读导致请求无法正常发起。

2. 拦截器异步逻辑的错误处理漏洞

当前拦截器的onError方法中,异步操作(如刷新Token)执行时未正确阻断错误传递流程,导致业务层的try/catch先捕获到401错误,之后拦截器的异步逻辑才继续执行,造成流程混乱。


解决步骤

步骤1:修复拦截器的异步错误处理逻辑

确保拦截器所有分支都调用handler.resolve或handler.reject,避免请求挂起或错误提前传递:

@override
void onError(DioError err, ErrorInterceptorHandler handler) async {
  if (err.response?.statusCode == 401) {
    log('++++++ interceptor error ++++++');

    if (await SecureStorage.loadAccessToken() == '') {
      handler.reject(err);
      return;
    }

    bool isTokenRefreshed = await AuthApi.refreshToken();

    if (isTokenRefreshed) {
      RequestOptions origin = err.response!.requestOptions;
      String token = await SecureStorage.loadAccessToken();
      origin.headers["Authorization"] = "Bearer $token";

      try {
        dynamic requestData = origin.data;
        // 针对Multipart请求重构FormData
        if (origin.headers['Content-Type']?.contains('multipart/form-data') == true) {
          final formParams = origin.extra['formDataParams'];
          if (formParams != null) {
            requestData = FormData.fromMap({
              'upload_file': MultipartFile.fromBytes(
                formParams['imageBytes'],
                filename: formParams['imageName'],
                contentType: MediaType('media_content', formParams['imageExtension']),
              ),
              'disclaimer': formParams['disclaimer'],
            });
          }
        }

        final Response response = await DioClient.request(
          url: origin.path,
          data: requestData,
          options: Options(
            headers: origin.headers,
            method: origin.method,
          ),
        );
        handler.resolve(response);
      } catch (e) {
        handler.reject(err);
      }
    } else {
      handler.reject(err);
    }
  } else {
    handler.reject(err);
  }
}

步骤2:在上传请求中保存FormData原始参数

修改uploadImage方法,将构建FormData的原始数据存入请求的extra字段,方便拦截器重试时重构请求体:

static Future uploadImage(PlatformFile image, String disclaimer,
      {String? imageTitle}) async {
    String imageExtension = image.extension!;
    String imageName = '${imageTitle ?? 'image'}.$imageExtension';

    final formData = FormData.fromMap({
      'upload_file': MultipartFile.fromBytes(
        image.bytes!,
        filename: imageName,
        contentType: MediaType('media_content', imageExtension),
      ),
      'disclaimer': disclaimer,
    });

    try {
      final response = await DioClient.post(
        url: Endpoint.images,
        data: formData,
        options: Options(
          headers: {
            'Content-Type': 'multipart/form-data',
          },
          // 保存原始参数到extra
          extra: {
            'formDataParams': {
              'imageBytes': image.bytes,
              'imageName': imageName,
              'disclaimer': disclaimer,
              'imageExtension': imageExtension,
            },
          },
        ),
      );

      return response.data;
    } on DioError catch (err) {
      ToastMessage.apiError(err);
      log('DioError uploadImage response: ${ToastMessage.message}');
    }
  }

额外优化建议

  • 将super.onRequest(options, handler)替换为handler.next(options),这是Dio拦截器的标准写法,更适配异步流程处理。
  • 可封装通用的FormData重试工具类,避免每个上传请求重复编写参数保存逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 14:25:05