cfug / dio

A powerful HTTP client for Dart and Flutter, which supports global settings, Interceptors, FormData, aborting and canceling a request, files uploading and downloading, requests timeout, custom adapters, etc.
https://dio.pub
MIT License
12.47k stars 1.51k forks source link

Locking interceptors doesn't work when multiple requests are enqueued #590

Closed josh-burton closed 2 years ago

josh-burton commented 4 years ago

New Issue Checklist

Issue Info

Info Value
Platform Name e.g. flutter
Platform Version e.g. master
Dio Version e.g. 3.0.7
Android Studio / Xcode Version e.g. IntelliJ
Repro rate e.g. all the time (100%)
Repro with our demo prj No

Issue Description and Steps

I'm using an interceptor with locks to lock the interceptor while a token is being refreshed. If multiple requests are enqueued while the lock is active, once it becomes unlocked, all of the requests run at once, rather than executing sequentially.

pedromassango commented 4 years ago

I'm having the same issue here. @athornz did you found a solution for this?

pedromassango commented 4 years ago

Don't stale this issue. It wasn't fixed yet.

josh-burton commented 4 years ago

Still a problem

lts1610 commented 4 years ago

I'm having the same issue. :(

pedromassango commented 4 years ago

Don't state yet

Xgamefactory commented 4 years ago

this is very important problem. please share solution if someone have or alternative package.

na-si commented 4 years ago

I have the same problem. I used the same example as here [https://github.com/flutterchina/dio/issues/50]. When I have few requests with authorization header, every request with invalid token (status 401) do refresh token. Looks like

dio.interceptor.request.lock(); dio.interceptor.response.lock();

works separately for every request, but not for whole bunch of requests.

vh13294 commented 4 years ago

I kinda found a work around using static class. So every request would be statically invoke, not a new instance.

class DioWrapper {
  static Dio http = Dio();

  static Dio get httpWithoutInterceptors {
    return Dio(http.options);
  }

  static Options defaultCache = buildCacheOptions(
    Duration(days: 7),
    forceRefresh: true,
  );

  static bool locked = false; // <---- HERE

  static bool get hasToken => (http.options.headers['Authorization'] != null);

  static DioCacheManager _cacheManager = DioCacheManager(
    CacheConfig(baseUrl: http.options.baseUrl),
  );

  static InterceptorsWrapper _mainInterceptor = InterceptorsWrapper(
    onRequest: (RequestOptions options) async {
      return options;
    },
    onResponse: (Response response) async {
      return response;
    },
    onError: (DioError e) async {
      if (e.response != null) {
        String $errorMessage = e.response.data['message'];
        if (e.response.statusCode == 401) {
          if (!locked) {
            locked = true; //unlock in AuthModel
            Get.snackbar(
              'Session Expired',
              'Please Login Again!',
              duration: Duration(seconds: 10),
            );
            logout();
          }
        } else {
          Get.defaultDialog(
            title: 'Error',
            onCancel: () => {},
            textCancel: 'Close',
            cancelTextColor: Colors.red,
            buttonColor: Colors.red,
            middleText: $errorMessage,
          );
        }
        return http.reject($errorMessage);
      } else {
        Get.snackbar(
          'Sever Error',
          'Please Check Connection!',
          duration: Duration(seconds: 10),
        );
        return http.reject(e.message);
      }
    },
  );

  static void setupDioWrapperHttp() {
    http.options.headers['X-Requested-With'] = 'XMLHttpRequest';
    http.options.baseUrl = 'https://api';
    http.interceptors.add(_mainInterceptor);
    http.interceptors.add(_cacheManager.interceptor);
    http.interceptors.add(LogInterceptor(
      request: false,
      requestHeader: false,
      responseHeader: false,
    ));
  }

  static void addTokenHeader(String token) {
    http.options.headers['Authorization'] = 'Bearer $token';
  }

  static Future<void> checkTokenInSharedPreference() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String token = prefs.getString('apiToken');
    if (token != null) {
      addTokenHeader(token);
    }
  }

  static void logout() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.remove('apiToken');
    addTokenHeader(null);
    Get.offAllNamed(RouterTable.login);
  }
}

main

void main() {
  FluroRouter.setupRouter();
  DioWrapper.setupDioWrapperHttp();
  runApp(HomePage());
}

usage

DioWrapper.http.get(...);
DioWrapper.locked = false;
pedromassango commented 4 years ago

Yes but, this need to be fixed by the Dio team, and seems that they are not interessed to fix this

Xgamefactory commented 4 years ago

Yes but, this need to be fixed by the Dio team, and seems that they are not interessed to fix this

Yes. looks like this package abandoned.

wendux commented 4 years ago

I have the same problem. I used the same example as here [https://github.com/[/issues/50](https://github.com/flutterchina/dio/issues/50)]. When I have few requests with authorization header, every request with invalid token (status 401) do refresh token. Looks like

dio.interceptor.request.lock(); dio.interceptor.response.lock();

works separately for every request, but not for whole bunch of requests.

First, Please make sure all multiple requests are Initiated by the same one dio instance.

Then, Please check out: https://github.com/flutterchina/dio/blob/master/example/interceptor_lock.dart https://github.com/flutterchina/dio/blob/master/dio/test/interceptor_test.dart

wendux commented 4 years ago

multiple requests

Why executing sequential is need? What is the senior ? If you really need to execute sequentially, you can do this as follows:

// enter `onResponse` one by one
 ...
onResponse: (){
 dio.interceptors.responseLock.lock();

  // do anything
  ...
 dio.interceptors.responseLock.unlock();
}
hongfeiyang commented 3 years ago

I have the same issue, definitely not fixed in 3.0.10

YuriyBereguliak commented 3 years ago

Hello everyone. I am faced with the same problem. Please check whether you didn't miss the code line:

dio.interceptors.errorLock.lock();
/// Refresh token here
dio.interceptors.errorLock.unlock();

And it is working with multiple requests.

dio version: 3.0.10

hongfeiyang commented 3 years ago

Hello everyone. I am faced with the same problem. Please check whether you didn't miss the code line:

dio.interceptors.errorLock.lock();
/// Refresh token here
dio.interceptors.errorLock.unlock();

And it is working with multiple requests.

dio version: 3.0.10

Yea I think I missed the error lock, I only locked request and response

kateile commented 3 years ago

@YuriyBereguliak , @hongfeiyang Hi, I tried errorLock.lock() without any luck. In my case where can I put it? Demo code below

class AuthInterceptor extends Interceptor {
  @override
  Future onError(DioError error) async {
    //If user is unauthorized
    if (error?.response?.statusCode == 401) {
      //Clearing all shared preferences
      final prefs = await SharedPreferences.getInstance();
      await prefs.clear();

      Application.navigatorKey.currentState.pushAndRemoveUntil(
        MaterialPageRoute(
          builder: (context) => AuthPage(),
        ),
        (route) => false,
      );
    }

    print('enddddddd');
    return super.onError(error);
  }
}
hongfeiyang commented 3 years ago

@kateile

class AuthInterceptor extends Interceptor {
  @override
  Future onError(DioError error) async {
    //If user is unauthorized
    if (error?.response?.statusCode == 401) {
      //Clearing all shared preferences
      final prefs = await SharedPreferences.getInstance();
      await prefs.clear();
      // lock error, response, request here
      ...
      // silently refresh token here
      ...
      // unlock error, response, request here 
      // In your case, you seem to redirect user to login page after detecting an invalid token, so you don't seem to need this lock. This lock is meant for refreshing token silently, in my personal option 
      Application.navigatorKey.currentState.pushAndRemoveUntil(
        MaterialPageRoute(
          builder: (context) => AuthPage(),
        ),
        (route) => false,
      );
    }

    print('enddddddd');
    return super.onError(error);
  }
}
kateile commented 3 years ago

@hongfeiyang . I am using graphql api. and I am using cookie/session based authentication so user needs to reenter password so that new session could be gennerated.

Even by using two clients I still have problem. @wendux may you provide a little help here?

  final dio = Dio(
    BaseOptions(
      connectTimeout: 3 * 1000,
    ),
  );

  final authDio = Dio()..options = dio.options;

  if (!kIsWeb) {
    final directory = await getApplicationSupportDirectory();
    final cookieJar = PersistCookieJar(dir: directory.path);

    dio.interceptors.add(CookieManager(cookieJar));
    authDio.interceptors.add(CookieManager(cookieJar));
  }

  dio.interceptors.add(
    InterceptorsWrapper(
      onError: (error) async {
        final _lockDio = () {
          dio.lock();
          dio.interceptors.responseLock.lock();
          dio.interceptors.errorLock.lock();
        };

        //If user is unauthorized
        if (error?.response?.statusCode == 401) {
          _lockDio();

          //Clearing all shared preferences
          final prefs = await SharedPreferences.getInstance();
          await prefs.clear();

          Application.navigatorKey.currentState.pushAndRemoveUntil(
            MaterialPageRoute(
              builder: (context) =>
                  AuthPage(), //todo message field to show 'your session expired'
            ),
            (route) => false,
          );
        }
        return error;
      },
    ),
  );

  authDio.interceptors.add(
    InterceptorsWrapper(
      onResponse: (response) async {
        if (response.statusCode == 200) {
          dio.unlock();
          dio.interceptors.responseLock.unlock();
          dio.interceptors.errorLock.unlock();

          return response;
        }
      },
    ),
  );
YuriyBereguliak commented 3 years ago

Hello @kateile. Actually, in case, if you may want to update the access token (OAuth) you need to lock (request, response, error) and then updating the token. Only after that unlock all and repeat the last request.

hongfeiyang commented 3 years ago

@kateile I recommend you to use one interceptor only to handle lock and unlock. Does you authentication page also uses the same dio instance? If so, since you are already locking the response after a 401, your authentication response will not be received and you will not be able to unlock (I am suspecting)

kateile commented 3 years ago

@hongfeiyang my AuthPage now(just temporary) submits requests using authDio and the rest of pages use dio. I actually tried two clients today to see if my problem would be solved but it is not. All redirections when 401 is returned by server works just fine. But after 401 requests are sent and server responds well(I see it in logs) but dio freezes there. I think the part where I am missing is how to restore dio to its original state after it faces 401 and redirection taking place

  final dio = Dio(
    BaseOptions(
      connectTimeout: 3 * 1000,
    ),
  );

  if (!kIsWeb) {
    final directory = await getApplicationSupportDirectory();
    final cookieJar = PersistCookieJar(dir: directory.path);

    dio.interceptors.add(CookieManager(cookieJar));
  }

  dio.interceptors.add(
    InterceptorsWrapper(
      onError: (error) async {
        if (error?.response?.statusCode == 401) {
          //If this this then dio no longer propagates response to bloc/UI even through they are all 200
          final prefs = await SharedPreferences.getInstance();
          await prefs.clear();

          Application.navigatorKey.currentState.pushAndRemoveUntil(
            MaterialPageRoute(
              builder: (context) => AuthPage(),
            ),
            (route) => false,
          );
        }
      },
    ),
  );

@YuriyBereguliak We don't use Oauth2. Our app user needs to reenter password again to be authenticated. And after redirect user to auth page the UI just freezes even after 200 from server because dio won't hande response after 401 trauma it faced some milliseconds ago. and for it work user must quit app and restart it again

YuriyBereguliak commented 3 years ago

@kateile Have you unlocked Dio before authentication? Because from your code you locked it. image

Looks like it is problem in your app architecture, but not in Dio.

hongfeiyang commented 3 years ago

@hongfeiyang my AuthPage now(just temporary) submits requests using authDio and the rest of pages use dio. I actually tried two clients today to see if my problem would be solved but it is not. All redirections when 401 is returned by server works just fine. But after 401 requests are sent and server responds well(I see it in logs) but dio freezes there. I think the part where I am missing is how to restore dio to its original state after it faces 401 and redirection taking place


  final dio = Dio(

    BaseOptions(

      connectTimeout: 3 * 1000,

    ),

  );

  if (!kIsWeb) {

    final directory = await getApplicationSupportDirectory();

    final cookieJar = PersistCookieJar(dir: directory.path);

    dio.interceptors.add(CookieManager(cookieJar));

  }

  dio.interceptors.add(

    InterceptorsWrapper(

      onError: (error) async {

        if (error?.response?.statusCode == 401) {

          //If this this then dio no longer propagates response to bloc/UI even through they are all 200

          final prefs = await SharedPreferences.getInstance();

          await prefs.clear();

          Application.navigatorKey.currentState.pushAndRemoveUntil(

            MaterialPageRoute(

              builder: (context) => AuthPage(),

            ),

            (route) => false,

          );

        }

      },

    ),

  );

@YuriyBereguliak We don't use Oauth2. Our app user needs to reenter password again to be authenticated. And after redirect user to auth page the UI just freezes even after 200 from server because dio won't hande response after 401 trauma it faced some milliseconds ago. and for it work user must quit app and restart it again

This is why I don't recommend you to use lock in this case. You could use onError in an interceptor to redirect your user to login page if you noticed the token is expired and therefore cleared from sharedPreference. Subsequent request will check for token and redirect user to login page if user is not already at login page. Once you got a new token, store it in shared preference and disable redirection

This way all your requests and responses can be handled properly for redirection

kateile commented 3 years ago

@YuriyBereguliak yeah I locked the first instance, but I unlock it when the second instance got 200

kateile commented 3 years ago

Strange enough is that I always get response from server

      onResponse: (res){
        print('res :$res'); //This always prints results
        return res;
      }

Let me build sample server and app then I will let you see it for yourself

hongfeiyang commented 3 years ago

@YuriyBereguliak yeah I locked the first instance, but I unlock it when the second instance got 200

Lock and unlock works on a single dio instance, remember not to use two dio instances and interceptors are triggered linearly so first interceptor will get triggered first and if it decides to return or unlock, then the second interceptor can get activated

YuriyBereguliak commented 3 years ago

@kateile I totally agree with @hongfeiyang. I do not recommend using a lock too. Also, I do not see a reason to have two instances of Dio. You can handle such situation in two ways:

  1. Interceptor -> onError callback
  2. catchError((DioError) e) - for request
kateile commented 3 years ago

@YuriyBereguliak I use single instance of dio like I have described here

I was just playing around with two instance to see what would happen on test branch. I will be back with repo for you to reproduce this

YuriyBereguliak commented 3 years ago

After reviewing your code, I see that the return statement is skipped for OnError. Try to add it. image image

Example image

kateile commented 3 years ago

I think there is something wrong in our project. I tried to reproduce here but everything works as expected in that sample. I will try to figure out what is wrong. And you guys are right with flow we don't need locks. Thanks for your time @YuriyBereguliak @hongfeiyang

Edit: I finally figure it out. Our navigation relies on bloc pattern and I was just doing navigation without informing bloc. So the state remained the same that's why I could not login after 401 because in bloc it was still success.

joshbenaron commented 3 years ago

Hey, Is there any update on this issue? I use a different Dio instance for each bloc I have (about 10 blocs!). I seem to be able to make one request every 30 seconds. But if I do 2 requests with only a few seconds between, it never really sends....

aradhwan commented 3 years ago

I'm facing the same issue when having multiple requests, my code is shown below, I am using two clients one for all requests except for refresh token I am using different client. Am I doing something wrong?

class RefreshTokenInterceptor extends Interceptor {
  final Dio originalDio;

  RefreshTokenInterceptor(Dio dio)
      : assert(dio != null),
        originalDio = dio;

  @override
  Future onError(DioError e) async {
    final request = e.request;
    if (e?.response?.statusCode != 401 || !Urls.requiresAuth(url: request?.uri?.toString()?.toLowerCase())) return e;
    originalDio.lock();
    originalDio.interceptors.responseLock.lock();
    originalDio.interceptors.errorLock.lock();
    final shouldRetry = await _shouldRetry();
    originalDio.unlock();
    originalDio.interceptors.responseLock.unlock();
    originalDio.interceptors.errorLock.unlock();
    if (shouldRetry) {
      return _generateRequest(e);
    }
    return e;
  }

  Future<bool> _shouldRetry() async {
    final refreshResponse = await ApiRepo().refreshToken();
    return refreshResponse.status;
  }

  _generateRequest(DioError e) async {
    final options = e.response.request;
    options?.data['token'] = (await DiskRepo().getTokens()).first;
    return originalDio.request(options.path, options: options);
  }
}
AnnaPS commented 3 years ago

I have a problem with multiple requests enqueued... they work on android devices, but in IOs devices only do one of the requests and the others are never executed... Any suggestion?

Fleximex commented 3 years ago

I am facing the same issue where, in cases of multiple requests that run in parellel fail because the tokens are expired (401), the refreshToken method will be called for every request in the onError interceptor. Locking the Dio instance and the requestLock, responseLock and errorLock seem to do nothing at all for me. Very disappointing that the Dio package seems abandoned or at least under low priority maintenance. This is a basic feature that should work. For me there is no alternative networking package because I need a lot of the extra features Dio has over the other networking packages.

Anyway. I sort of solved it by using the Queue package: https://pub.dev/packages/queue

By creating a Queue instance which allows only one Future to run at once (a serial queue) I was able to make sure that refreshing the tokens and checking if the tokens were refreshed was done in succession.

In the onError interceptor callback:

if (error.response?.statusCode != 401) {
  return handler.reject(error);
}
// Check for if the token were successfully refreshed
bool success = false;
await queue.add(() async {
  // refreshTokens returns true when it has successfully retrieved the new tokens.
  // When the Authorization header of the original request differs from the current Authorization header of the Dio instance,
  // it means the tokens where refreshed by the first request in the queue and the refreshTokens call does not have to be made.
  success = error.requestOptions.headers['Authorization'] ==
          dio.options.headers['Authorization']
      ? await refreshTokens()
      : true;
});
if (!success) {
  return handler.reject(error);
}
// Retry the request here, with the new tokens

The above is using Dio 4.0.0 which uses a handler to handle failed requests.

One thing to keep in mind is that if the first request in the queue does not manage to refresh the tokens the second request in the queue will still try to refresh the tokens. This should not be a problem if you handle failed requests properly.

I hope this can help some people.

wendux commented 2 years ago

I think QueuedInterceptor can help, This issue will be closed, please trans to https://github.com/flutterchina/dio/issues/1308

HiemIT commented 1 year ago

This is my code, I'm trying to close the request to perform the task according to the latest version of Dio, Can you help me?

void setupInterceptors() {
    _dio.interceptors.add(InterceptorsWrapper(onRequest:
        (RequestOptions options, RequestInterceptorHandler handler) async {
      logger.log('[${options.method}] - ${options.uri}');
      if (_accessToken!.isEmpty) {
        _dio.interceptors.requestLock.locked;

        return SharedPreferences.getInstance().then((sharedPreferences) {
          TokenManager().load(sharedPreferences);

          logger.log('calling with access token: $_accessToken');
          options.headers['Authorization'] = 'Bearer $_accessToken';
//          options.headers['DeviceUID'] = TrackEventRepo().uid();

          _dio.unlock();
          return handler.next(options); //continue
        });
      }

      options.headers['Authorization'] = 'Bearer $_accessToken';
      return handler.next(options); //continue
      // If you want to resolve the request with some custom data,
      // you can return a `Response` object or return `dio.resolve(data)`.
      // If you want to reject the request with a error message,
      // you can return a `DioError` object or return `dio.reject(errMsg)`
    }, onResponse: (response, handler) {
      return handler.next(response); // continue
    }, onError: (DioError e, ErrorInterceptorHandler handler) async {
      logger.log(e.response.toString());

      // TODO: refresh token && handle error
      return handler.next(e); //continue
    }));
  }
sebastianbuechler commented 9 months ago

Since #1308 got closed down with open requests for example here's a working example for opaque token refreshment with concurrent network requests. The example can easily be applied for non-opaque tokens with built-in expiration by using the onRequest handler instead of the onError handler.

DioHandler implementation:

import 'dart:convert';
import 'package:dio/dio.dart';

const baseUrlApi = "https://api.yourcompany.com";
const baseUrlToken = "https://auth.youcompany.com";
const apiTokenHeader = "apiToken";

class DioHandler {
  DioHandler({
    required this.requestDio,
    required this.tokenDio,
  });
  late Dio requestDio;
  late Dio tokenDio;
  String? apiToken;

  void setup() {
    requestDio.options.baseUrl = baseUrlApi;
    tokenDio.options.baseUrl = baseUrlToken;

    requestDio.interceptors.add(
      QueuedInterceptorsWrapper(
        onRequest: _onRequestHandler,
        onError: _onErrorHandler,
      ),
    );
  }

  void _onRequestHandler(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    print('[ON REQUEST HANDLER] start handling request: ${options.uri}');

    if (apiToken == null) {
      print('[ON REQUEST HANDLER] no token available, request token now');
      final result = await tokenDio.get('/token');

      if (result.statusCode != null && result.statusCode! ~/ 100 == 2) {
        final body = jsonDecode(result.data) as Map<String, dynamic>?;

        if (body != null && body.containsKey('token')) {
          print('[ON REQUEST HANDLER] request token succeeded: $apiToken');
          options.headers[apiTokenHeader] = apiToken = body['token'];

          print(
              '[ON REQUEST HANDLER] continue to perform request: ${options.uri}');
          return handler.next(options);
        }
      }

      return handler.reject(
        DioException(requestOptions: result.requestOptions),
        true,
      );
    }

    options.headers['apiToken'] = apiToken;
    return handler.next(options);
  }

  Future<void> _retryOriginalRequest(
    RequestOptions options,
    ErrorInterceptorHandler handler,
  ) async {
    final originResult = await requestDio.fetch(options);
    if (originResult.statusCode != null &&
        originResult.statusCode! ~/ 100 == 2) {
      print('[ON ERROR HANDLER] request has been successfully re-sent');
      return handler.resolve(originResult);
    }
  }

  String? _extractToken(Response<dynamic> tokenResult) {
    String? token;
    if (tokenResult.statusCode != null && tokenResult.statusCode! ~/ 100 == 2) {
      final body = jsonDecode(tokenResult.data) as Map<String, dynamic>?;

      if (body != null && body.containsKey('token')) {
        token = body['token'];
      }
    }

    return token;
  }

  void _onErrorHandler(
    DioException error,
    ErrorInterceptorHandler handler,
  ) async {
    if (error.response?.statusCode == 401) {
      print('[ON ERROR HANDLER] used token expired');
      final options = error.response!.requestOptions;
      final tokenNeedsRefreshment = options.headers[apiTokenHeader] == apiToken;

      if (tokenNeedsRefreshment) {
        final tokenResult = await tokenDio.get('/token');
        final refreshedToken = _extractToken(tokenResult);

        if (refreshedToken == null) {
          print('[ON ERROR HANDLER] token refreshment failed');
          return handler.reject(DioException(requestOptions: options));
        }

        options.headers[apiTokenHeader] = apiToken = refreshedToken;
      } else {
        print(
            '[ON ERROR HANDLER] token has already been updated by another onError handler');
      }

      return await _retryOriginalRequest(options, handler);
    }
    return handler.next(error);
  }
}

Some basic unit tests:

import 'dart:convert';

import 'package:dio_example/dio_handler.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dio/dio.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';

void main() {
  late DioHandler dioHandler;
  late DioAdapter requestDioAdapter;
  late DioAdapter tokenDioAdapter;
  final List<String> tokenList = [
    "pxmun0kmxc",
    "aunerq1k3l",
    "x3nermkxc1",
    "yzD88xrdqh",
  ];

  void setUnauthorizedAsDefault() {
    requestDioAdapter.onGet(
      "/test",
      (request) => request.reply(401, {'message': 'unauthorized'}),
    );
    print('[REQUEST API] no valid token registered');
  }

  String registerToken(List<String> tokenList, int nextTokenIndex) {
    final newToken = tokenList[nextTokenIndex];

    requestDioAdapter.onGet(
      "/test",
      (request) => request.reply(200, {'message': 'ok'}),
      headers: {'apiToken': newToken},
    );
    return newToken;
  }

  void addNewTokenEndpoint() {
    var nextTokenIndex = 0;

    tokenDioAdapter.onGet(
      "/token",
      (request) => request.replyCallback(200, (RequestOptions requestOptions) {
        String newToken = registerToken(tokenList, nextTokenIndex);
        nextTokenIndex++;

        print('[TOKEN API] new token successfully generated: $newToken');
        return jsonEncode({'token': newToken});
      }),
    );
  }

  void invalidateToken() {
    requestDioAdapter.onGet(
      "/test",
      (request) => request.reply(401, {'message': 'unauthorized'}),
    );
    print('[TOKEN API] token invalidated');
  }

  setUp(() {
    final requestDio = Dio();
    final tokenDio = Dio();

    dioHandler = DioHandler(requestDio: requestDio, tokenDio: tokenDio);
    dioHandler.setup();

    requestDioAdapter = DioAdapter(dio: requestDio);
    setUnauthorizedAsDefault();

    tokenDioAdapter = DioAdapter(dio: tokenDio);
    addNewTokenEndpoint();
  });

  group('DioHandlerTest -', () {
    group("onRequestHandler", () {
      test('when no api token is available it should fetch one first',
          () async {
        expect(dioHandler.apiToken, null);

        print('[TEST] start request without valid token');
        final response = await dioHandler.requestDio.get('/test');

        expect(response.statusCode, 200);
        expect(dioHandler.apiToken, tokenList[0]);
      });

      test('when api token is available it should be used', () async {
        print('[TEST] start request with valid token');
        expect(dioHandler.apiToken, null);

        final newToken = registerToken(tokenList, 0);
        dioHandler.apiToken = newToken;
        expect(dioHandler.apiToken, tokenList[0]);

        final response = await dioHandler.requestDio.get('/test');
        expect(response.statusCode, 200);
        expect(dioHandler.apiToken, tokenList[0]);
      });
    });
    group("onErrorHandler", () {
      test('when api token is invalid it should be refreshed once', () async {
        print('[TEST] start request without valid token');
        final response = await dioHandler.requestDio.get('/test');
        expect(response.statusCode, 200);
        expect(dioHandler.apiToken, tokenList[0]);

        invalidateToken();

        print('[TEST] start concurrent requests without valid token');
        final responses = await Future.wait([
          dioHandler.requestDio.get('/test'),
          dioHandler.requestDio.get('/test'),
          dioHandler.requestDio.get('/test'),
          dioHandler.requestDio.get('/test'),
          dioHandler.requestDio.get('/test'),
        ]);

        responses.forEach((response) {
          expect(response.statusCode, 200);
        });
        expect(dioHandler.apiToken, tokenList[1]);
      });
    });
  });
}

Pubspec.yaml

name: dio_queued_interceptor_example
description: Example for the use of dio queued interceptor
version: 0.0.1
publish_to: "none"

environment:
  sdk: ">=3.0.0 <4.0.0"

dependencies:
  dio: ^5.4.0
  flutter_test:
    sdk: flutter

dev_dependencies:
  http_mock_adapter: ^0.6.1
  lints: any

@chandrabezzo @shawoozy @stngcoding @logispire

x-ji commented 5 months ago

Regarding the need to prevent concurrent requests from trying to refresh the multiple times, especially now that the lock mechanism is gone, this Stackoverflow question proved helpful to me: https://stackoverflow.com/questions/76228296/dio-queuedinterceptor-to-handle-refresh-token-with-multiple-requests You can find code sample on the SO page.

In short, the idea is to store a list of failed requests in the interceptor instance and retry all of them manually after the token refresh succeeds once. (In addition, a isRefreshing flag makes sure that only one refreshing attempt runs at a time.)