Repository: cfug/dio (dart)

Issue:
`DioException.toString()` should contain the request information
### Request Statement

When printing exception information, it is impossible to quickly locate the source of the request. Here are some examples:

DioException [connection timeout]: The request connection took longer than 0:00:30.000000. It was aborted.

The exception message only shows that a connection timeout occurred, but does not provide any information about which request timed out. This makes it difficult to quickly identify the problematic request when debugging.

Some additional context that would be useful:

The URL of the request
The HTTP method (GET, POST, etc)
Any identifiers like request ID
So a more helpful exception could be:

DioException [connection timeout]: GET https://example.com/api/v1/users took longer than 30s and was aborted.

By including details like the URL and request method in the exception, it becomes much faster to pinpoint where the problem is occurring.

### Solution Brainstorm

_No response_

Pull request description:
Resolves https://github.com/cfug/dio/issues/1949
Resolves https://github.com/cfug/dio/issues/2287

Developers may want the URL of the exception to know what endpoint has gone wrong, or they want to have fewer tips about what the status code means. If we open too many flags to support various logging content, it will be a mess eventually.
Introducing the `DioExceptionReadableStringBuilder` easily builds text content from the `DioException`.

### New Pull Request Checklist

- [x] I have read the [Documentation](https://pub.dev/documentation/dio/latest/)
- [x] I have searched for a similar pull request in the [project](https://github.com/cfug/dio/pulls) and found none
- [x] I have updated this branch with the latest `main` branch to avoid conflicts (via merge from master or rebase)
- [x] I have added the required tests to prove the fix/feature I'm adding
- [x] I have updated the documentation (if necessary)
- [x] I have run the tests without failures
- [x] I have updated the `CHANGELOG.md` in the corresponding package

Patch:
diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md
index 9a1ec45f2..3eafb3580 100644
--- a/dio/CHANGELOG.md
+++ b/dio/CHANGELOG.md
@@ -10,6 +10,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
 - Fixes boundary inconsistency in `FormData.clone()`.
 - Support `FileAccessMode` in `Dio.download` and `Dio.downloadUri` to change download file opening mode
 - Fix `ListParam` equality by using the `DeepCollectionEquality`.
+- Enables configuring the logging details of `DioException` globally and locally.
 
 ## 5.7.0
 
diff --git a/dio/lib/src/dio_exception.dart b/dio/lib/src/dio_exception.dart
index 69aac43d0..6dcdc375c 100644
--- a/dio/lib/src/dio_exception.dart
+++ b/dio/lib/src/dio_exception.dart
@@ -1,5 +1,6 @@
 import 'options.dart';
 import 'response.dart';
+import 'utils.dart' show warningLog;
 
 /// Deprecated in favor of [DioExceptionType] and will be removed in future major versions.
 @Deprecated('Use DioExceptionType instead. This will be removed in 6.0.0')
@@ -205,6 +206,14 @@ class DioException implements Exception {
   /// The error message that throws a [DioException].
   final String? message;
 
+  /// Users can customize the content of [toString] when thrown.
+  static DioExceptionReadableStringBuilder readableStringBuilder =
+      defaultDioExceptionReadableStringBuilder;
+
+  /// Each exception can be override with a customized builder or fallback to
+  /// the default [DioException.readableStringBuilder].
+  DioExceptionReadableStringBuilder? stringBuilder;
+
   /// Generate a new [DioException] by combining given values and original values.
   DioException copyWith({
     RequestOptions? requestOptions,
@@ -226,11 +235,12 @@ class DioException implements Exception {
 
   @override
   String toString() {
-    String msg = 'DioException [${type.toPrettyDescription()}]: $message';
-    if (error != null) {
-      msg += '\nError: $error';
+    try {
+      return stringBuilder?.call(this) ?? readableStringBuilder(this);
+    } catch (e, s) {
+      warningLog(e, s);
+      return defaultDioExceptionReadableStringBuilder(this);
     }
-    return msg;
   }
 
   /// Because of [ValidateStatus] we need to consider all status codes when
@@ -278,3 +288,20 @@ class DioException implements Exception {
     return buffer.toString();
   }
 }
+
+/// The readable string builder's signature of
+/// [DioException.readableStringBuilder].
+typedef DioExceptionReadableStringBuilder = String Function(DioException e);
+
+/// The default implementation of building a readable string of [DioException].
+String defaultDioExceptionReadableStringBuilder(DioException e) {
+  final buffer = StringBuffer(
+    'DioException [${e.type.toPrettyDescription()}]: '
+    '${e.message}',
+  );
+  if (e.error != null) {
+    buffer.writeln();
+    buffer.write('Error: ${e.error}');
+  }
+  return buffer.toString();
+}
diff --git a/dio/lib/src/utils.dart b/dio/lib/src/utils.dart
index 06d5aa94e..1a2e1d801 100644
--- a/dio/lib/src/utils.dart
+++ b/dio/lib/src/utils.dart
@@ -154,10 +154,10 @@ Map<String, V> caseInsensitiveKeyMap<V>([Map<String, V>? value]) {
 }
 
 // TODO(Alex): Provide a configurable property on the Dio class once https://github.com/cfug/dio/discussions/1982 has made some progress.
-void warningLog(String message, StackTrace stackTrace) {
+void warningLog(Object message, StackTrace stackTrace) {
   if (!kReleaseMode) {
     dev.log(
-      message,
+      message.toString(),
       level: 900,
       name: '🔔 Dio',
       stackTrace: stackTrace,


Test patch:
diff --git a/dio/test/exception_test.dart b/dio/test/exception_test.dart
index bfc0f95d7..740ceb5dc 100644
--- a/dio/test/exception_test.dart
+++ b/dio/test/exception_test.dart
@@ -52,4 +52,34 @@ void main() {
     },
     testOn: '!browser',
   );
+
+  test('DioExceptionReadableStringBuilder', () {
+    final requestOptions = RequestOptions(path: 'just/a/test', method: 'POST');
+    final exception = DioException(
+      requestOptions: requestOptions,
+      response: Response(requestOptions: requestOptions),
+      error: 'test',
+      message: 'test message',
+      stackTrace: StackTrace.current,
+    );
+    DioException.readableStringBuilder = (e) => 'Hey, Dio throws an exception: '
+        '${e.requestOptions.path}, '
+        '${e.requestOptions.method}, '
+        '${e.type}, '
+        '${e.error}, '
+        '${e.stackTrace}, '
+        '${e.message}';
+    expect(
+      exception.toString(),
+      'Hey, Dio throws an exception: '
+      'just/a/test, '
+      'POST, '
+      'DioExceptionType.unknown, '
+      'test, '
+      '${exception.stackTrace}, '
+      'test message',
+    );
+    exception.stringBuilder = (e) => 'Locally override: ${e.message}';
+    expect(exception.toString(), 'Locally override: test message');
+  });
 }