212 lines
7.7 KiB
Dart
Executable File
212 lines
7.7 KiB
Dart
Executable File
import 'dart:convert';
|
||
|
||
import 'package:dio/dio.dart';
|
||
|
||
extension DioExceptionX on DioException {
|
||
String get userMessage {
|
||
switch (type) {
|
||
case DioExceptionType.connectionTimeout:
|
||
case DioExceptionType.sendTimeout:
|
||
case DioExceptionType.receiveTimeout:
|
||
return 'Connection timeout. Please check your internet.';
|
||
case DioExceptionType.badResponse:
|
||
final statusCode = response?.statusCode;
|
||
if (statusCode == 401) return 'Unauthorized. Please log in again.';
|
||
if (statusCode == 404) return 'Resource not found.';
|
||
if (statusCode != null && statusCode >= 500) {
|
||
return 'Server error (Status $statusCode).';
|
||
}
|
||
return 'Request failed with status $statusCode.';
|
||
case DioExceptionType.cancel:
|
||
return 'Request was cancelled.';
|
||
case DioExceptionType.unknown:
|
||
return 'An unexpected error occurred.';
|
||
default:
|
||
return 'Request failed.';
|
||
}
|
||
}
|
||
|
||
String toDebugString() {
|
||
final buffer = StringBuffer();
|
||
buffer.writeln('🚨 [DioException] ${type.name}');
|
||
buffer.writeln('📤 Request: ${requestOptions.method} ${requestOptions.uri}');
|
||
if (response != null) {
|
||
buffer.writeln('📥 Response: ${response!.statusCode}');
|
||
if (response!.data != null) {
|
||
buffer.writeln(' Body: ${_formatData(response!.data)}');
|
||
}
|
||
} else if (error != null) {
|
||
buffer.writeln('⚠️ Error: ${error.toString()}');
|
||
}
|
||
return buffer.toString().trim();
|
||
}
|
||
|
||
String _formatData(dynamic data) {
|
||
if (data == null) return 'null';
|
||
if (data is Map || data is List) {
|
||
try {
|
||
const encoder = JsonEncoder.withIndent(' ');
|
||
return encoder.convert(data);
|
||
} catch (_) {
|
||
return data.toString();
|
||
}
|
||
}
|
||
return data.toString();
|
||
}
|
||
}
|
||
|
||
extension DiscordDioException on DioException {
|
||
/// Generates a Markdown formatted error report suitable for Discord messages.
|
||
String toDiscordMarkdown() {
|
||
final emoji = _getEmoji();
|
||
final title = _getFriendlyTitle();
|
||
final message = userMessage;
|
||
|
||
final buffer = StringBuffer();
|
||
buffer.writeln('$emoji **$title**');
|
||
buffer.writeln();
|
||
buffer.writeln(message);
|
||
buffer.writeln();
|
||
buffer.writeln('`${requestOptions.method} ${requestOptions.uri}`');
|
||
buffer.writeln();
|
||
|
||
// Technical details
|
||
buffer.writeln('```yaml');
|
||
buffer.writeln('Type: ${type.name}');
|
||
if (response != null) {
|
||
buffer.writeln('Status: ${response!.statusCode}');
|
||
}
|
||
if (error != null) {
|
||
final errorStr = error.toString();
|
||
buffer.writeln('Error: ${errorStr.length > 200 ? '${errorStr.substring(0, 200)}...' : errorStr}');
|
||
}
|
||
buffer.writeln('```');
|
||
|
||
// Ensure whole message fits in Discord's 2000‑char limit
|
||
var result = buffer.toString().trim();
|
||
const maxLength = 2000;
|
||
if (result.length > maxLength) {
|
||
result = '${result.substring(0, maxLength - 3)}...';
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// Human‑readable message, enhanced with the server’s own error message if available.
|
||
String get userMessage {
|
||
print("Server message: ${response?.data}");
|
||
final base = _baseUserMessage();
|
||
final server = _getServerMessage();
|
||
if (server == null || server.isEmpty) return base;
|
||
if (base.contains(server)) return base; // avoid duplication
|
||
return '$base — $server';
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Private helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
String _baseUserMessage() {
|
||
switch (type) {
|
||
case DioExceptionType.connectionTimeout:
|
||
case DioExceptionType.sendTimeout:
|
||
case DioExceptionType.receiveTimeout:
|
||
return 'The connection timed out.';
|
||
|
||
case DioExceptionType.connectionError:
|
||
final errorStr = error?.toString().toLowerCase() ?? '';
|
||
if (errorStr.contains('semaphore timeout') || errorStr.contains('timed out')) {
|
||
return 'Connection timed out. The server took too long to respond.';
|
||
}
|
||
if (errorStr.contains('connection refused')) {
|
||
return 'Connection refused. The server is likely offline.';
|
||
}
|
||
if (errorStr.contains('failed host lookup')) {
|
||
return 'DNS Lookup Failed. Could not reach the host.';
|
||
}
|
||
return 'A network connection error occurred.';
|
||
|
||
case DioExceptionType.badResponse:
|
||
final statusCode = response?.statusCode;
|
||
// Common client errors
|
||
if (statusCode == 400) return 'Bad Request (400).';
|
||
if (statusCode == 401) return 'Unauthorized (401). Authentication failed.';
|
||
if (statusCode == 403) return 'Forbidden (403). You lack permission.';
|
||
if (statusCode == 404) return 'Not Found (404). The resource does not exist.';
|
||
if (statusCode == 422) return 'Validation Error (422). Some fields are invalid.';
|
||
if (statusCode == 429) return 'Too Many Requests (429). You are being rate‑limited.';
|
||
// Server errors
|
||
if (statusCode != null && statusCode >= 500) {
|
||
if (statusCode == 503) return 'Service Unavailable (503).';
|
||
return 'Server Error ($statusCode). Please try again later.';
|
||
}
|
||
return 'Request failed with status $statusCode.';
|
||
|
||
case DioExceptionType.cancel:
|
||
return 'The request was cancelled.';
|
||
|
||
case DioExceptionType.unknown:
|
||
final errorStr = error?.toString().toLowerCase() ?? '';
|
||
if (errorStr.contains('socketexception')) {
|
||
if (errorStr.contains('connection refused')) return 'Server connection refused.';
|
||
if (errorStr.contains('failed host lookup')) return 'Could not find the server address.';
|
||
if (errorStr.contains('semaphore timeout')) return 'Network timeout.';
|
||
return 'Network connection unstable.';
|
||
}
|
||
return 'An unexpected error occurred.';
|
||
|
||
default:
|
||
return 'Request failed.';
|
||
}
|
||
}
|
||
|
||
String? _getServerMessage() {
|
||
final data = response?.data;
|
||
if (data is Map) {
|
||
// Try common top‑level keys
|
||
for (final key in ['message', 'error', 'detail', 'description']) {
|
||
final val = data[key];
|
||
if (val is String && val.trim().isNotEmpty) return val.trim();
|
||
}
|
||
// Nested errors map (e.g. { "errors": { "email": ["is taken"] } })
|
||
final errors = data['errors'];
|
||
if (errors is Map) {
|
||
for (final entry in errors.entries) {
|
||
final v = entry.value;
|
||
if (v is List && v.isNotEmpty) return '${entry.key}: ${v.first}';
|
||
if (v is String && v.isNotEmpty) return '${entry.key}: $v';
|
||
}
|
||
}
|
||
} else if (data is List && data.isNotEmpty) {
|
||
if (data.first is Map) {
|
||
return _getServerMessageFromMap(data.first as Map);
|
||
}
|
||
} else if (data is String && data.isNotEmpty) {
|
||
// Plain text body – use it directly, but keep it short
|
||
return data.length > 200 ? '${data.substring(0, 200)}...' : data;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Helper for when the first element of a list is a map
|
||
String? _getServerMessageFromMap(Map map) {
|
||
print("Server has map: ${map}");
|
||
for (final key in ['message', 'error', 'detail']) {
|
||
final val = map[key];
|
||
if (val is String && val.trim().isNotEmpty) {
|
||
return val.trim();
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
String _getFriendlyTitle() {
|
||
if (type == DioExceptionType.badResponse) return 'API Error';
|
||
return 'Network Error';
|
||
}
|
||
|
||
String _getEmoji() {
|
||
if (type == DioExceptionType.badResponse) return '⚠️';
|
||
return '⛔';
|
||
}
|
||
}
|