feat: implement centralized human-readable error messaging for Dio exceptions and improve API response parsing
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:real_estate_mobile/config/app_config.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
|
// 1. Try to parse a structured error from the JSON response body
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
final data = response.data;
|
// response.data can be Map (already decoded), String (raw JSON), or null.
|
||||||
if (data is Map<String, dynamic>) {
|
Map<String, dynamic>? dataMap;
|
||||||
final rawMessage = data['message'] ?? data['error'];
|
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
|
final message = rawMessage is List
|
||||||
? (rawMessage).join(', ')
|
? (rawMessage).join(', ')
|
||||||
: rawMessage?.toString();
|
: rawMessage?.toString();
|
||||||
|
|
||||||
if (message != null && message.isNotEmpty) {
|
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>))
|
?.map((e) => FieldError.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
@@ -275,6 +288,14 @@ class ApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dynamic _tryDecodeJson(String raw) {
|
||||||
|
try {
|
||||||
|
return jsonDecode(raw);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String _friendlyMessageFor(DioException error) {
|
String _friendlyMessageFor(DioException error) {
|
||||||
// Network-level failures take priority over status codes
|
// Network-level failures take priority over status codes
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
|
|||||||
@@ -10,6 +10,31 @@ import 'package:real_estate_mobile/features/auth/data/models/user_model.dart';
|
|||||||
class AuthRepository {
|
class AuthRepository {
|
||||||
final Dio _dio = ApiClient.instance.dio;
|
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({
|
Future<Map<String, dynamic>> login({
|
||||||
required String email,
|
required String email,
|
||||||
required String password,
|
required String password,
|
||||||
@@ -42,11 +67,9 @@ class AuthRepository {
|
|||||||
'user': authResponse.user,
|
'user': authResponse.user,
|
||||||
};
|
};
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.error is ApiException) {
|
if (e.error is ApiException) throw e.error as ApiException;
|
||||||
throw e.error as ApiException;
|
|
||||||
}
|
|
||||||
throw ApiException(
|
throw ApiException(
|
||||||
message: e.message ?? 'Login failed',
|
message: _friendlyError(e, 'Login failed'),
|
||||||
statusCode: e.response?.statusCode,
|
statusCode: e.response?.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,7 +95,7 @@ class AuthRepository {
|
|||||||
} on DioException catch (e) {
|
} 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(
|
throw ApiException(
|
||||||
message: e.message ?? '2FA verification failed',
|
message: _friendlyError(e, '2FA verification failed'),
|
||||||
statusCode: e.response?.statusCode,
|
statusCode: e.response?.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,7 +121,7 @@ class AuthRepository {
|
|||||||
} on DioException catch (e) {
|
} 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(
|
throw ApiException(
|
||||||
message: e.message ?? 'Backup code verification failed',
|
message: _friendlyError(e, 'Backup code verification failed'),
|
||||||
statusCode: e.response?.statusCode,
|
statusCode: e.response?.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -110,11 +133,9 @@ class AuthRepository {
|
|||||||
final data = response.data['data'] as Map<String, dynamic>;
|
final data = response.data['data'] as Map<String, dynamic>;
|
||||||
return UserModel.fromJson(data);
|
return UserModel.fromJson(data);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.error is ApiException) {
|
if (e.error is ApiException) throw e.error as ApiException;
|
||||||
throw e.error as ApiException;
|
|
||||||
}
|
|
||||||
throw ApiException(
|
throw ApiException(
|
||||||
message: e.message ?? 'Failed to fetch user',
|
message: _friendlyError(e, 'Failed to fetch user'),
|
||||||
statusCode: e.response?.statusCode,
|
statusCode: e.response?.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -168,7 +189,7 @@ class AuthRepository {
|
|||||||
} on DioException catch (e) {
|
} 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(
|
throw ApiException(
|
||||||
message: e.message ?? 'Social login failed',
|
message: _friendlyError(e, 'Social login failed'),
|
||||||
statusCode: e.response?.statusCode,
|
statusCode: e.response?.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -186,11 +207,9 @@ class AuthRepository {
|
|||||||
final data = response.data['data'] as Map<String, dynamic>;
|
final data = response.data['data'] as Map<String, dynamic>;
|
||||||
return data;
|
return data;
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.error is ApiException) {
|
if (e.error is ApiException) throw e.error as ApiException;
|
||||||
throw e.error as ApiException;
|
|
||||||
}
|
|
||||||
throw ApiException(
|
throw ApiException(
|
||||||
message: e.message ?? 'Registration failed',
|
message: _friendlyError(e, 'Registration failed'),
|
||||||
statusCode: e.response?.statusCode,
|
statusCode: e.response?.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user