Flutter+Retrofit架构下FormatException无法定位问题,如何捕获原始响应排查?
我之前也踩过一模一样的坑——Retrofit解析JSON时炸FormatException,但完全找不到是哪个API出的问题,结合你的MVVM场景,给你几个亲测好用的解决办法:
一、给Dio加全局拦截器,抓所有请求的原始响应
Retrofit本质是基于Dio封装的,所以给Dio实例加个拦截器,就能在Retrofit解析JSON前,拿到每一个请求的原始响应数据,这是定位问题最直接的方式。
直接抄这个拦截器代码就行:
class ApiLoggingInterceptor extends InterceptorsWrapper { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { print('--> ${options.method} ${options.uri}'); super.onRequest(options, handler); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { print('<-- ${response.statusCode} ${response.requestOptions.uri}'); print('原始响应体: ${response.data}'); // 重点!这里是Retrofit解析前的原始数据 super.onResponse(response, handler); } @override void onError(DioError err, ErrorInterceptorHandler handler) { print('<-- 错误请求: ${err.requestOptions.uri}'); print('错误原因: ${err.message}'); if (err.response != null) { print('响应状态码: ${err.response?.statusCode}'); print('错误响应体: ${err.response?.data}'); // 专门揪出FormatException if (err.error is FormatException) { print('🔥 找到问题了!这个API返回了无效JSON: ${err.requestOptions.uri}'); print('无效JSON内容: ${err.response?.data}'); } } super.onError(err, handler); } }
然后把这个拦截器挂到你的Dio实例上:
final dio = Dio(); dio.interceptors.add(ApiLoggingInterceptor()); // 再把这个dio传给Retrofit的DataSource工厂 final notificationDatasource = NotificationRemoteDatasource(dio); final scheduleDatasource = ScheduleRemoteDatasource(dio);
这样不管是启动时自动调用的API,还是用户操作触发的,所有请求的原始响应都会打印在控制台,一旦炸FormatException,拦截器会直接给你标红哪个API出的问题,连无效JSON的内容都能看到。
二、优化全局错误处理,精准捕获Retrofit的解析错误
你已经加了FlutterError.onError和PlatformDispatcher的错误处理,但还要专门处理DioError——因为Retrofit的JSON解析错误会被包装成DioError,类型是DioErrorType.response,里面的error字段就是原始的FormatException。
修改你的ErrorUtils,加上对Dio错误的判断:
class ErrorUtils { static void onErrorsAndExceptions(Object error, StackTrace? stackTrace) { if (error is DioError) { final dioError = error; print('来自API的Dio错误: ${dioError.requestOptions.uri}'); if (dioError.response != null) { print('响应状态: ${dioError.response?.statusCode}'); print('原始响应内容: ${dioError.response?.data}'); } // 精准定位解析错误 if (dioError.error is FormatException) { print('⚠️ JSON解析失败:'); print('出问题的API: ${dioError.requestOptions.uri}'); print('无效JSON: ${dioError.response?.data}'); print('调用栈: $stackTrace'); } } else { // 处理其他类型的错误 print('错误内容: $error'); print('调用栈: $stackTrace'); } } }
这样即使全局错误触发,也能直接定位到出问题的API。
三、针对void返回的API,避免不必要的JSON解析
你有几个返回Future<void>的API(比如createMeal、reschedule),如果后端返回空字符串或非JSON内容,Retrofit还是会硬着头皮尝试解析成void对应的JSON,这也会炸FormatException。
解决办法是把这些API的返回类型改成Future<Response<dynamic>>,跳过Retrofit的自动解析:
// 修改NotificationRemoteDatasource的reschedule方法 @POST(NotificationApiConstants.reschedule) Future<Response<dynamic>> reschedule({ @Path('notificationID') required String notificationID, @Body() required RescheduleModel body }); // 修改ScheduleRemoteDatasource的createMeal方法 @POST(ScheduleApiConstants.createMeal) @MultiPart() Future<Response<dynamic>> createMeal({ @Part(name: 'meal_type') required String mealType, @Part(name: 'description') required String description, @Part(name: 'image') required File image, });
然后在ViewModel里调用时,直接判断状态码就行:
final response = await _notificationDatasource.reschedule(notificationID: id, body: body); if (response.statusCode == 200 || response.statusCode == 204) { // 处理成功逻辑 } else { // 处理错误 }
这样就避免了Retrofit对空响应/非JSON响应的强制解析,减少无意义的FormatException。
内容来源于stack exchange

