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

