From cdb918fae80dac8fd45c2182851b94d228ea1b90 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Fri, 10 Apr 2026 17:35:59 +0530 Subject: [PATCH] feat: implement centralized human-readable error messaging for Dio exceptions and improve API response parsing --- lib/core/network/api_client.dart | 29 ++++++++++-- lib/features/auth/data/auth_repository.dart | 49 ++++++++++++++------- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index 3f572b5..e9a3a52 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:real_estate_mobile/config/app_config.dart'; @@ -234,15 +235,27 @@ class ApiClient { // 1. Try to parse a structured error from the JSON response body if (response != null) { - final data = response.data; - if (data is Map) { - final rawMessage = data['message'] ?? data['error']; + // response.data can be Map (already decoded), String (raw JSON), or null. + Map? dataMap; + if (response.data is Map) { + dataMap = response.data as Map; + } else if (response.data is Map) { + dataMap = Map.from(response.data as Map); + } else if (response.data is String) { + try { + final decoded = _tryDecodeJson(response.data as String); + if (decoded is Map) dataMap = decoded; + } catch (_) {} + } + + if (dataMap != null) { + final rawMessage = dataMap['message'] ?? dataMap['error']; final message = rawMessage is List ? (rawMessage).join(', ') : rawMessage?.toString(); if (message != null && message.isNotEmpty) { - final errors = (data['errors'] as List?) + final errors = (dataMap['errors'] as List?) ?.map((e) => FieldError.fromJson(e as Map)) .toList() ?? []; @@ -275,6 +288,14 @@ class ApiClient { ); } + dynamic _tryDecodeJson(String raw) { + try { + return jsonDecode(raw); + } catch (_) { + return null; + } + } + String _friendlyMessageFor(DioException error) { // Network-level failures take priority over status codes switch (error.type) { diff --git a/lib/features/auth/data/auth_repository.dart b/lib/features/auth/data/auth_repository.dart index 0080e1b..2807f7d 100644 --- a/lib/features/auth/data/auth_repository.dart +++ b/lib/features/auth/data/auth_repository.dart @@ -10,6 +10,31 @@ import 'package:real_estate_mobile/features/auth/data/models/user_model.dart'; class AuthRepository { final Dio _dio = ApiClient.instance.dio; + /// Extract a user-friendly message from a DioException. + /// NEVER returns Dio's raw technical text — always a short human sentence. + static String _friendlyError(DioException e, String fallback) { + // If our interceptor already wrapped it, use that message + if (e.error is ApiException) return (e.error as ApiException).message; + // Status-code based fallback + switch (e.response?.statusCode) { + case 400: return 'The request was invalid. Please check your input.'; + case 401: return 'Invalid email or password.'; + case 403: return 'You don\'t have permission to perform this action.'; + case 404: return 'Not found.'; + case 409: return 'This action conflicts with existing data.'; + case 429: return 'Too many attempts. Please wait and try again.'; + } + if (e.type == DioExceptionType.connectionTimeout || + e.type == DioExceptionType.receiveTimeout || + e.type == DioExceptionType.sendTimeout) { + return 'Request timed out. Please check your connection.'; + } + if (e.type == DioExceptionType.connectionError) { + return 'Unable to reach the server. Check your internet connection.'; + } + return fallback; + } + Future> login({ required String email, required String password, @@ -42,11 +67,9 @@ class AuthRepository { 'user': authResponse.user, }; } on DioException catch (e) { - if (e.error is ApiException) { - throw e.error as ApiException; - } + if (e.error is ApiException) throw e.error as ApiException; throw ApiException( - message: e.message ?? 'Login failed', + message: _friendlyError(e, 'Login failed'), statusCode: e.response?.statusCode, ); } @@ -72,7 +95,7 @@ class AuthRepository { } on DioException catch (e) { if (e.error is ApiException) throw e.error as ApiException; throw ApiException( - message: e.message ?? '2FA verification failed', + message: _friendlyError(e, '2FA verification failed'), statusCode: e.response?.statusCode, ); } @@ -98,7 +121,7 @@ class AuthRepository { } on DioException catch (e) { if (e.error is ApiException) throw e.error as ApiException; throw ApiException( - message: e.message ?? 'Backup code verification failed', + message: _friendlyError(e, 'Backup code verification failed'), statusCode: e.response?.statusCode, ); } @@ -110,11 +133,9 @@ class AuthRepository { final data = response.data['data'] as Map; return UserModel.fromJson(data); } on DioException catch (e) { - if (e.error is ApiException) { - throw e.error as ApiException; - } + if (e.error is ApiException) throw e.error as ApiException; throw ApiException( - message: e.message ?? 'Failed to fetch user', + message: _friendlyError(e, 'Failed to fetch user'), statusCode: e.response?.statusCode, ); } @@ -168,7 +189,7 @@ class AuthRepository { } on DioException catch (e) { if (e.error is ApiException) throw e.error as ApiException; throw ApiException( - message: e.message ?? 'Social login failed', + message: _friendlyError(e, 'Social login failed'), statusCode: e.response?.statusCode, ); } @@ -186,11 +207,9 @@ class AuthRepository { final data = response.data['data'] as Map; return data; } on DioException catch (e) { - if (e.error is ApiException) { - throw e.error as ApiException; - } + if (e.error is ApiException) throw e.error as ApiException; throw ApiException( - message: e.message ?? 'Registration failed', + message: _friendlyError(e, 'Registration failed'), statusCode: e.response?.statusCode, ); }