cfug/dio

A request with an interceptor, which produces an error, makes the code stuck.

yehorh opened this issue · 11 comments

Package

dio

Version

5.4.1

Operating-System

Android

Output of flutter doctor -v

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

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.

It seems I have broken the interface contract.
I have overridden synchronous functions with asynchronous functions.

The override_on_non_overriding_member lint hasn't helped me this time.

the same problem is happening with our project too.

We're currently experiencing the same issue using the following interactor to refresh an access token and retry the request, using Dio v5.4.3+1. The request never finishes executing and no exceptions are caught in the try/catch block.

try {
  final body = {...};
  final options = Options(extra: {'authenticated': true}, validateStatus: (status) => status == 200);
  final response = await _dio.post('...', data: body, options: options);
  // We never get here
} on DioException catch (error, trace) {
  // We never get here
}
class AuthInterceptor extends Interceptor {
  final Dio _dio;
  final Future<String?> Function() _getAccessToken;

  AuthInterceptor({
    required Dio dio,
    required Future<String?> Function() getAccessToken,
  })  : _dio = dio,
        _getAccessToken = getAccessToken;

  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    // If the request is not authenticated, continue without adding the Authorization header
    if (options.extra['authenticated'] == false) {
      return handler.next(options);
    }

    // Try to retrieve a valid (non-expired) access token for the Authorization header.
    // If no token could be retrieved, we don't add the header.
    final String? accessToken = await _getAccessToken();
    if (accessToken != null) {
      options.headers['Authorization'] = 'Bearer $accessToken';
    }

    // Continue request
    return handler.next(options);
  }

  @override
  FutureOr<void> onError(DioException error, ErrorInterceptorHandler handler) async {
    final Response? response = error.response;

    // We're only interested in handling the 401 header here. Let other errors pass through.
    if (response == null || response.statusCode != 401) {
      return handler.next(error);
    }

    final RequestOptions options = response.requestOptions;

    // If the request was already retried we back off and let the error pass through.
    if (options.extra['retried'] == true) {
      return handler.next(error); // also tried handler.reject(error) with same result
    }

    // Add flag to indicate that we retried the request, to prevent infinite retries.
    options.extra['retried'] = true;

    // Retry the request. A refresh token will be added to the request in the [onRequest]
    // handler, when available.
    return handler.resolve(await _dio.fetch(options));
  }
}

@sdgroot

is this part from your code using the same _dio instance as the main app thread (final response = await _dio.post('...', data: body, options: options);?

// Retry the request. A refresh token will be added to the request in the [onRequest]
// handler, when available.
return handler.resolve(await _dio.fetch(options));

for token interceptors it's better to use QueuedInterceptor and separate Dio instance which do retries, see https://github.com/cfug/dio/blob/main/example/lib/queued_interceptor_crsftoken.dart