import 'package:dio/dio.dart'; /// Mirrors backend/pfm-web-app/src/lib/http-status.ts - keep the two in sync. const Map _httpReasonPhrases = { 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 409: 'Conflict', 413: 'Payload Too Large', 422: 'Unprocessable Entity', 429: 'Too Many Requests', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', }; /// Structured representation of an API/network failure, built from the /// backend's `{ status: "error", error: { statusCode, code, message } }` /// envelope or synthesized for network-level failures (timeout, no /// connection). This is the single object every catch block should read /// instead of scraping `DioException.toString()`. class ApiException implements Exception { final int? statusCode; final String code; final String message; const ApiException({this.statusCode, required this.code, required this.message}); String get reasonPhrase { if (statusCode == null) return code; return _httpReasonPhrases[statusCode] ?? 'Error'; } /// Formatted like an HTTP status line, e.g. "404 Not Found: Document not found". String get displayMessage { if (statusCode == null) return message; return '$statusCode $reasonPhrase: $message'; } /// Unwraps any thrown object into an [ApiException]. This is the one call /// every catch block should use to get a consistent, displayable error. static ApiException from(Object error) { if (error is ApiException) return error; if (error is DioException) { if (error.error is ApiException) return error.error as ApiException; return fromDioException(error); } // Strip Dart's default `Exception: `/`Error: ` toString() prefix so plain // `throw Exception("...")` call sites still display just the message. final raw = error.toString(); final message = raw.replaceFirst(RegExp(r'^(Exception|Error): '), ''); return ApiException(code: 'UNKNOWN', message: message); } static ApiException fromDioException(DioException e) { if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.sendTimeout || e.type == DioExceptionType.receiveTimeout) { return const ApiException(code: 'TIMEOUT', message: 'Koneksi timeout. Silakan coba lagi.'); } if (e.type == DioExceptionType.connectionError) { return const ApiException(code: 'NETWORK_ERROR', message: 'Tidak ada koneksi internet.'); } final response = e.response; if (response != null) { final data = response.data; if (data is Map && data['error'] is Map) { final errorBody = data['error'] as Map; return ApiException( statusCode: (errorBody['statusCode'] as num?)?.toInt() ?? response.statusCode, code: errorBody['code']?.toString() ?? 'UNKNOWN', message: errorBody['message']?.toString() ?? 'Tidak diketahui', ); } // Fallback for responses that don't (yet) use the standardized envelope. String fallbackMessage = 'Tidak diketahui'; if (data is Map && data['message'] != null) { fallbackMessage = data['message'].toString(); } else if (data is String && data.isNotEmpty) { fallbackMessage = data.length > 100 ? '${data.substring(0, 100)}...' : data; } return ApiException( statusCode: response.statusCode, code: 'UNKNOWN', message: fallbackMessage, ); } return const ApiException(code: 'UNKNOWN', message: 'Terjadi kesalahan yang tidak terduga'); } }