Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
1 task done
josh-burton opened this issue Dec 5, 2019 · 37 comments
Closed
1 task done

Comments

@josh-burton
Copy link
Contributor

New Issue Checklist

  • I have searched for a similar issue in the project and found none

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.

@stale stale bot added the stale label Jan 4, 2020
@pedromassango
Copy link

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

@stale stale bot removed the stale label Jan 9, 2020
@stale stale bot added the stale label Feb 8, 2020
@pedromassango
Copy link

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

@stale stale bot removed the stale label Feb 8, 2020
@stale stale bot added the stale label Mar 9, 2020
@josh-burton
Copy link
Contributor Author

Still a problem

@stale stale bot removed the stale label Mar 9, 2020
@lts1610
Copy link

lts1610 commented Mar 25, 2020

I'm having the same issue. :(

@stale stale bot added the stale label Apr 24, 2020
@pedromassango
Copy link

pedromassango commented Apr 24, 2020

Don't state yet

@stale stale bot closed this as completed May 1, 2020
@Xgamefactory
Copy link

Xgamefactory commented May 4, 2020

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

@na-si
Copy link

na-si commented May 25, 2020

I have the same problem. I used the same example as here [https://github.com//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
Copy link

vh13294 commented Jun 6, 2020

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
Copy link

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

@Xgamefactory
Copy link

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
Copy link
Contributor

wendux commented Aug 7, 2020

I have the same problem. I used the same example as here [https://github.com//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
Copy link
Contributor

wendux commented Aug 7, 2020

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();
}

@wendux wendux reopened this Aug 7, 2020
@stale stale bot removed the stale label Aug 7, 2020
@wendux wendux added the pinned label Aug 7, 2020
@cfug cfug deleted a comment from stale bot Aug 7, 2020
@cfug cfug deleted a comment from stale bot Aug 7, 2020
@cfug cfug deleted a comment from stale bot Aug 7, 2020
@cfug cfug deleted a comment from stale bot Aug 7, 2020
@hongfeiyang
Copy link

hongfeiyang commented Nov 30, 2020

I have the same issue, definitely not fixed in 3.0.10

@YuriyBereguliak
Copy link

YuriyBereguliak commented Feb 1, 2021

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
Copy link

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
Copy link

kateile commented Apr 5, 2021

@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
Copy link

hongfeiyang commented Apr 5, 2021

@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
Copy link

kateile commented Apr 5, 2021

@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
Copy link

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
Copy link

@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
Copy link

kateile commented Apr 5, 2021

@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
Copy link

YuriyBereguliak commented Apr 5, 2021

@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
Copy link

@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
Copy link

kateile commented Apr 5, 2021

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

@kateile
Copy link

kateile commented Apr 5, 2021

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
Copy link

hongfeiyang commented Apr 5, 2021

@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
Copy link

YuriyBereguliak commented Apr 5, 2021

@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
Copy link

kateile commented Apr 5, 2021

@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
Copy link

YuriyBereguliak commented Apr 5, 2021

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

Example
image

@kateile
Copy link

kateile commented Apr 5, 2021

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
Copy link

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
Copy link

aradhwan commented Jun 3, 2021

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
Copy link

AnnaPS commented Jun 29, 2021

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
Copy link

Fleximex commented Aug 18, 2021

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
Copy link
Contributor

wendux commented Oct 31, 2021

I think QueuedInterceptor can help, This issue will be closed, please trans to #1308

@wendux wendux closed this as completed Oct 31, 2021
@wendux wendux removed the pinned label Nov 15, 2021
@HiemIT
Copy link

HiemIT commented Apr 11, 2023

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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests