Reputation: 1012
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
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
Reputation: 14380
Update 13/02/23:
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
Reputation: 61
To instantly solve this problem just comment out the "connectTimeOut" field from DioBaseOptions as follows:
connectTimeout: 30000,
Upvotes: 0
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
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
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
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.
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