Repository: cfug/dio

Issue:
A request with an interceptor, which produces an error, makes the code stuck.
### Package

dio

### Version

5.4.1

### Operating-System

Android

### Output of `flutter doctor -v`

```shell
flutter doctor -v
[✓] Flutter (Channel stable, 3.19.3, on macOS 14.4 23E214 darwin-arm64, locale en-UA)
    • Flutter version 3.19.3 on channel stable at /Users/yehorh/opt/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ba39319843 (6 days ago), 2024-03-07 15:22:21 -0600
    • Engine revision 2e4ba9c6fb
    • Dart version 3.3.1
    • DevTools version 2.31.1

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0-rc1)
    • Android SDK at /Users/yehorh/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0-rc1
    • Java binary at: /Users/yehorh/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.3)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15E204a
    • CocoaPods version 1.14.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2023.2)
    • Android Studio at /Users/yehorh/Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)

[✓] IntelliJ IDEA Ultimate Edition (version 2023.3.5)
    • IntelliJ at /Users/yehorh/Applications/IntelliJ IDEA Ultimate.app
    • Flutter plugin version 78.3.1
    • Dart plugin version 233.14888

[✓] VS Code (version 1.87.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.84.0

[✓] Connected device (4 available)            
    • sdk gphone64 arm64 (mobile) • emulator-5554             • android-arm64  • Android 13 (API 33) (emulator)
    • yiPhone (mobile)            • 00008101-000E28880AD2001E • ios            • iOS 17.4 21E219
    • macOS (desktop)             • macos                     • darwin-arm64   • macOS 14.4 23E214 darwin-arm64
    • Chrome (web)                • chrome                    • web-javascript • Google Chrome 122.0.6261.128

[✓] Network resources
    • All expected network resources are available.

• No issues found!
```


### Dart Version

3.3.1

### Steps to Reproduce

Here is a minimal example; I can't understand why the error isn't caught in any way, and everything that follows the request never executes.

tried on Android and macOS

```dart
import 'package:dio/dio.dart';

Future<void> main() async {
  final dio = Dio();

  dio.interceptors.add(InterceptorWithError());

  try {
    final response = await dio.get('https://google.com');
  } catch (e, st) {
    print('Unreachable code');
  }

  print('Unreachable code too');
}

class InterceptorWithError extends Interceptor {
  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    throw Exception('some error');
    return handler.next(options);
  }

  @override
  Future<void> onResponse(
    Response response,
    ResponseInterceptorHandler handler,
  ) async {
    handler.next(response);
  }

  @override
  Future<void> onError(
    DioException err,
    ErrorInterceptorHandler handler,
  ) async {
    handler.next(err);
  }
}
```

### Expected Result

Errors from broken requests must be caught.

### Actual Result

The program stops when executing a request, and the subsequent code becomes unreachable.




Fix patch:
diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md
index f1ab769e6..7c0b42bb6 100644
--- a/dio/CHANGELOG.md
+++ b/dio/CHANGELOG.md
@@ -6,6 +6,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
 ## Unreleased
 
 - Fix `receiveTimeout` throws exception after the request has been cancelled.
+- Catch sync/async exceptions in interceptors' handlers.
 
 ## 5.4.1
 
diff --git a/dio/lib/src/dio_mixin.dart b/dio/lib/src/dio_mixin.dart
index 9ec104f1a..9c5507b36 100644
--- a/dio/lib/src/dio_mixin.dart
+++ b/dio/lib/src/dio_mixin.dart
@@ -375,29 +375,31 @@ abstract class DioMixin implements Dio {
     // Convert the request interceptor to a functional callback in which
     // we can handle the return value of interceptor callback.
     FutureOr Function(dynamic) requestInterceptorWrapper(
-      InterceptorSendCallback interceptor,
+      InterceptorSendCallback cb,
     ) {
       return (dynamic incomingState) async {
         final state = incomingState as InterceptorState;
         if (state.type == InterceptorResultType.next) {
           return listenCancelForAsyncTask(
             requestOptions.cancelToken,
-            Future(() {
-              final requestHandler = RequestInterceptorHandler();
-              interceptor(state.data as RequestOptions, requestHandler);
-              return requestHandler.future;
+            Future(() async {
+              final handler = RequestInterceptorHandler();
+              final callback = cb(state.data as RequestOptions, handler);
+              if (callback is Future) {
+                await callback;
+              }
+              return handler.future;
             }),
           );
-        } else {
-          return state;
         }
+        return state;
       };
     }
 
     // Convert the response interceptor to a functional callback in which
     // we can handle the return value of interceptor callback.
     FutureOr<dynamic> Function(dynamic) responseInterceptorWrapper(
-      InterceptorSuccessCallback interceptor,
+      InterceptorSuccessCallback cb,
     ) {
       return (dynamic incomingState) async {
         final state = incomingState as InterceptorState;
@@ -405,10 +407,13 @@ abstract class DioMixin implements Dio {
             state.type == InterceptorResultType.resolveCallFollowing) {
           return listenCancelForAsyncTask(
             requestOptions.cancelToken,
-            Future(() {
-              final responseHandler = ResponseInterceptorHandler();
-              interceptor(state.data as Response, responseHandler);
-              return responseHandler.future;
+            Future(() async {
+              final handler = ResponseInterceptorHandler();
+              final callback = cb(state.data as Response, handler);
+              if (callback is Future) {
+                await callback;
+              }
+              return handler.future;
             }),
           );
         } else {
@@ -420,16 +425,19 @@ abstract class DioMixin implements Dio {
     // Convert the error interceptor to a functional callback in which
     // we can handle the return value of interceptor callback.
     FutureOr<dynamic> Function(Object) errorInterceptorWrapper(
-      InterceptorErrorCallback interceptor,
+      InterceptorErrorCallback cb,
     ) {
       return (error) {
         final state = error is InterceptorState
             ? error
             : InterceptorState(assureDioException(error, requestOptions));
         Future<InterceptorState> handleError() async {
-          final errorHandler = ErrorInterceptorHandler();
-          interceptor(state.data, errorHandler);
-          return errorHandler.future;
+          final handler = ErrorInterceptorHandler();
+          final callback = cb(state.data, handler);
+          if (callback is Future) {
+            await callback;
+          }
+          return handler.future;
         }
 
         // The request has already been cancelled,
@@ -437,15 +445,15 @@ abstract class DioMixin implements Dio {
         if (state.data is DioException &&
             state.data.type == DioExceptionType.cancel) {
           return handleError();
-        } else if (state.type == InterceptorResultType.next ||
+        }
+        if (state.type == InterceptorResultType.next ||
             state.type == InterceptorResultType.rejectCallFollowing) {
           return listenCancelForAsyncTask(
             requestOptions.cancelToken,
             Future(handleError),
           );
-        } else {
-          throw error;
         }
+        throw error;
       };
     }
 
@@ -495,12 +503,13 @@ abstract class DioMixin implements Dio {
       future = future.catchError(errorInterceptorWrapper(fun));
     }
     // Normalize errors, converts errors to [DioException].
-    return future.then<Response<T>>((data) {
+    try {
+      final data = await future;
       return assureResponse<T>(
         data is InterceptorState ? data.data : data,
         requestOptions,
       );
-    }).catchError((Object e) {
+    } catch (e) {
       final isState = e is InterceptorState;
       if (isState) {
         if (e.type == InterceptorResultType.resolve) {
@@ -508,7 +517,7 @@ abstract class DioMixin implements Dio {
         }
       }
       throw assureDioException(isState ? e.data : e, requestOptions);
-    });
+    }
   }
 
   Future<Response<dynamic>> _dispatchRequest<T>(RequestOptions reqOpt) async {
diff --git a/dio/lib/src/interceptor.dart b/dio/lib/src/interceptor.dart
index bca8981b2..5259e8ba5 100644
--- a/dio/lib/src/interceptor.dart
+++ b/dio/lib/src/interceptor.dart
@@ -207,19 +207,19 @@ class Interceptor {
 }
 
 /// The signature of [Interceptor.onRequest].
-typedef InterceptorSendCallback = void Function(
+typedef InterceptorSendCallback = FutureOr<void> Function(
   RequestOptions options,
   RequestInterceptorHandler handler,
 );
 
 /// The signature of [Interceptor.onResponse].
-typedef InterceptorSuccessCallback = void Function(
+typedef InterceptorSuccessCallback = FutureOr<void> Function(
   Response<dynamic> response,
   ResponseInterceptorHandler handler,
 );
 
 /// The signature of [Interceptor.onError].
-typedef InterceptorErrorCallback = void Function(
+typedef InterceptorErrorCallback = FutureOr<void> Function(
   DioException error,
   ErrorInterceptorHandler handler,
 );


Test patch:
diff --git a/dio/test/interceptor_test.dart b/dio/test/interceptor_test.dart
index b757e4f71..097515ecd 100644
--- a/dio/test/interceptor_test.dart
+++ b/dio/test/interceptor_test.dart
@@ -318,6 +318,31 @@ void main() {
       expect(response.data['errCode'], 0);
     });
 
+    test('Caught exceptions before handler called', () async {
+      final dio = Dio();
+      const errorMsg = 'interceptor error';
+      dio.interceptors.add(
+        InterceptorsWrapper(
+          // TODO(EVERYONE): Remove the ignorance once we migrated to a higher version of Dart.
+          // ignore: void_checks
+          onRequest: (response, handler) {
+            throw UnsupportedError(errorMsg);
+          },
+        ),
+      );
+      expect(
+        dio.get('https://www.cloudflare.com'),
+        throwsA(
+          isA<DioException>().having(
+            (dioException) => dioException.error,
+            'Exception',
+            isA<UnsupportedError>()
+                .having((e) => e.message, 'message', errorMsg),
+          ),
+        ),
+      );
+    });
+
     group(ImplyContentTypeInterceptor, () {
       Dio createDio() {
         final dio = Dio();