feat: implement centralized human-readable error messaging for Dio exceptions and improve API response parsing

This commit is contained in:
pradeepkumar
2026-04-10 17:35:59 +05:30
parent fe521d64f5
commit cdb918fae8
2 changed files with 59 additions and 19 deletions

View File

@@ -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<String, dynamic>) {
final rawMessage = data['message'] ?? data['error'];
// response.data can be Map (already decoded), String (raw JSON), or null.
Map<String, dynamic>? dataMap;
if (response.data is Map<String, dynamic>) {
dataMap = response.data as Map<String, dynamic>;
} else if (response.data is Map) {
dataMap = Map<String, dynamic>.from(response.data as Map);
} else if (response.data is String) {
try {
final decoded = _tryDecodeJson(response.data as String);
if (decoded is Map<String, dynamic>) 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<dynamic>?)
final errors = (dataMap['errors'] as List<dynamic>?)
?.map((e) => FieldError.fromJson(e as Map<String, dynamic>))
.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) {

View File

@@ -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<Map<String, dynamic>> 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<String, dynamic>;
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<String, dynamic>;
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,
);
}