Vi.
Vi.

Reputation: 1012

Flutter Dio interceptor Error: Bad state: Future already completed

I have an interceptor to send jwt token and to use the refresh_token endpoint when the jwt expires. With an expired jwt I get

Error: Bad state: Future already completed

error, but the request is processed right anyway. In the console I see one successful response and one with 401 error afterward. How can I solve this issue?

custom_interceptor.dart

class CustomInterceptor extends DefaultInterceptor {
  ISecureStorage secureStorageService = ISecureStorage();

  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    LoginModel loginModel = await secureStorageService.readLoginModel();

    options.headers = {
      "Content-type": "application/json",
      "Authorization": "Bearer ${loginModel.access_token}"
    };
    return super.onRequest(options, handler);
  }

  @override
  void onError(err, handler) async {
    if (err.response?.statusCode == 401) {
      final Dio _dio = DioConfig().dio;
      LoginModel loginModel = await secureStorageService.readLoginModel();
      Uri uri = Uri.https(
          "$BASE_URL", "/refresh_token_url");
      try {
        await _dio.postUri(uri, data: {
          "refresh_token": loginModel.refresh_token,
          "grant_type": "refresh_token"
        }).then((value) async {
          if (value?.statusCode == 200) {

            await secureStorageService.deleteLoginModel();
            LoginModel newLoginData = LoginModel.fromJson(value.data);
            await secureStorageService.saveLoginModel(loginModel: newLoginData);
            
            err.requestOptions.headers["Authorization"] =
                "Bearer " + newLoginData.refresh_token;

            final opts = new Options(
                method: err.requestOptions.method,
                headers: err.requestOptions.headers);
            final cloneReq = await _dio.request(err.requestOptions.path,
                options: opts,
                data: err.requestOptions.data,
                queryParameters: err.requestOptions.queryParameters);
            return handler.resolve(cloneReq);
          }
          return err;
        });
        return super.onError(err, handler);
      } catch (e, st) {
        print("ERROR: " + e);
        print("STACK: " + st.toString());
        return super.onError(err, handler);
      }
    } else {
      return super.onError(err, handler);
    }
  }
}
class DefaultInterceptor extends Interceptor {
  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    print(
        'REQUEST[${options.method}] => PATH: ${options.path} | DATA => ${options.data} | JWT => ${options.headers}');
    return super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print(
        'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path} | DATA => ${response.data}');
    super.onResponse(response, handler);
    return;
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    print(
        'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path} | SENT_DATA => ${err.requestOptions.data} | RECEIVED_DATA => ${err.response?.data}');
    return super.onError(err, handler);
  }
}

dio_config.dart

class DioConfig {
    static DioConfig _singletonHttp;
    Dio _dio;

    get dio => _dio;

    factory DioConfig() {
        _singletonHttp ??= DioConfig._singleton();
        return _singletonHttp;
    }

    DioConfig._singleton() {
        _dio = Dio();
    }

    dispose() {
        _dio.close();
    }
}

i_secure_storage.dart

abstract class ISecureStorage {

    factory ISecureStorage() => getSecureStorage();

    Future<LoginModel> readLoginModel() async => LoginModel.empty;

    Future<bool> saveLoginModel({LoginModel loginModel}) async => false;

    Future<bool> deleteLoginModel() async => false;
}

web_secure_storage.dart

ISecureStorage getSecureStorage() => WebSecureStorageService();

class WebSecureStorageService implements ISecureStorage {

    final String _loginData = 'loginData';
    html.Storage webStorage = html.window.localStorage;

    @override
    Future<LoginModel> readLoginModel() async {
        return webStorage[_loginData] == null
                ? LoginModel.empty
                : LoginModel.fromJson(jsonDecode(webStorage[_loginData]));
    }

    @override
    Future<bool> saveLoginModel({ LoginModel loginModel}) async {
        webStorage[_loginData] = jsonEncode(loginModel);
        return true;
    }

    @override
    Future<bool> deleteLoginModel() async {
        webStorage.remove(_loginData);
        return true;
    }
}

mobile_secure_storage.dart

ISecureStorage getSecureStorage() => MobileSecureStorageService();

class MobileSecureStorageService implements ISecureStorage {
    final String _loginModel = 'loginModel';

    FlutterSecureStorage storage = const FlutterSecureStorage();

    @override
    Future<LoginModel> readLoginModel() async {
        try {
            dynamic _loginData = await storage.read(key: _loginModel);
            return _loginData == null ? LoginModel.empty : LoginModel.fromJson(jsonDecode(_loginData));
        } on PlatformException catch (ex) {
            throw PlatformException(code: ex.code, message: ex.message);
        }
    }

    @override
    Future<bool> saveLoginModel({LoginModel loginModel}) async {
        try {
            await storage.write(key: _loginModel, value: jsonEncode(loginModel));
            return true;
        } on PlatformException catch (ex) {
            throw PlatformException(code: ex.code, message: ex.message);
        }
    }

    @override
    Future<bool> deleteLoginModel() async {
        try {
            await storage.delete(key: _loginModel);
            return true;
        } on PlatformException catch (ex) {
            throw PlatformException(code: ex.code, message: ex.message);
        }
    }
}

EDIT:

IN MY CASE the problem was in the first

return super.onError(err, handler);

It must be return null;

So I got it working

Upvotes: 5

Views: 12990

Answers (7)

Majed DH
Majed DH

Reputation: 1301

It may be obvious but in my case, after spending couple of days debugging the issue, the cause was calling the handler multiple times.

An ErrorInterceptorHandler should be called once, be it handler.resolve, handler.reject, or handler.next. Only one of them should be called exactly once, not zero times, not more than one, for each request or the error Future already completed will happen.

Upvotes: 5

David Miguel
David Miguel

Reputation: 14380

Update 13/02/23:

  • dio v5.0.0 finally contains a fix for this issue.

Details: At the end flutter-china has transferred the ownership of the dio repo to CFUG and all the changes from the diox hard fork have been merged into the original dio repo, including the fix for this issue.


Update 15/12/22: diox is a hard fork of dio made by CFUG group with the aim of keeping dio well maintained. In diox, this issue has already been fixed.


Original answer:

Related issue: https://github.com/flutterchina/dio/issues/1480

There are several open PRs that (try to) tackle this bug:

If you do not want to downgrade to dio 4.0.4 as other answers suggest, you can depend on some of these forks until one of them is merged into the official repository.

In my case, I've reviewed and tested @ipcjs's solution and seems to be working as expected:

dio:
  git:
    url: https://github.com/ipcjs/dio
    path: dio/
    ref: b77af132442bf3266ccf11b50ce909711455db3a

Upvotes: 2

Usama Kabir
Usama Kabir

Reputation: 61

To instantly solve this problem just comment out the "connectTimeOut" field from DioBaseOptions as follows:

connectTimeout: 30000,

Upvotes: 0

Wai Han Ko
Wai Han Ko

Reputation: 1043

To solve this error, I did like that

void onError(DioError err, ErrorInterceptorHandler handler) async {
  //Halding refresh token other logic
 
//Future.delay solve my error.
  Future.delayed(const Duration(seconds: 5), () => super.onError(err,handler));
}

Upvotes: -2

luckyhandler
luckyhandler

Reputation: 11329

For anyone else having this issue and it is not solved by only downgrading dio: Downgrade dio to 4.0.4 AND remove connectTimeout from your BaseOptions.

Upvotes: 4

Suman Poudyal
Suman Poudyal

Reputation: 11

class InterceptorsWrapper extends QueuedInterceptorsWrapper {
@override
void onRequest(RequestOptions options,RequestInterceptorHandler handler){
 log('send request:${options.baseUrl}${options.path}');

 final accessToken = Storage.instance.box.read("accessToken");

 options.headers['Authorization'] = 'Bearer $accessToken';

 super.onRequest(options, handler);
}

@override
void onError(DioError err, ErrorInterceptorHandler handler) {
switch (err.type) {
  case DioErrorType.connectTimeout:
  case DioErrorType.sendTimeout:
  case DioErrorType.receiveTimeout:
    throw DeadlineExceededException(err.requestOptions);
  case DioErrorType.response:
    switch (err.response?.statusCode) {
      case 400:
        throw BadRequestException(err.requestOptions);
      case 401:
        throw UnauthorizedException(err.requestOptions);
      case 404:
        throw NotFoundException(err.requestOptions);
      case 409:
        throw ConflictException(err.requestOptions);
      case 500:
        throw InternalServerErrorException(err.requestOptions);
    }
    break;
  case DioErrorType.cancel:
    break;
  case DioErrorType.other:
    throw NoInternetConnectionException(err.requestOptions);
}
super.onError(err, handler);
}
}
...
...

This is how I done my Dio Interceptor, you don't have to return anything in your void onRequest() simply call super.onRequest() and don't use handler instance in interceptor class like

return handler.resolve(cloneReq);

that part is already done inside onRequest(). I solved my problem in this way you can also try.

thank you.

Upvotes: 1

Hamza Khurshid
Hamza Khurshid

Reputation: 825

You are using Dio for the requests. Version 4.0.6 of Dio which is the most recent version as of today has this known issue. Please refer to the same on GitHub here.

Solution

Downgrade your Dio package to the last stable version that was known to not have this issue until a new version is released.

In your pubspec.yaml.

dio: 4.0.4

Then get packages again.

> flutter pub get

Upvotes: 6

Related Questions