Closed josh-burton closed 2 years ago
I'm having the same issue here. @athornz did you found a solution for this?
Don't stale this issue. It wasn't fixed yet.
Still a problem
I'm having the same issue. :(
Don't state yet
this is very important problem. please share solution if someone have or alternative package.
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.
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;
Yes but, this need to be fixed by the Dio team, and seems that they are not interessed to fix this
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.
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
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();
}
I have the same issue, definitely not fixed in 3.0.10
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
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
@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);
}
}
@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);
}
}
@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;
}
},
),
);
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.
@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)
@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
@kateile Have you unlocked Dio before authentication? Because from your code you locked it.
Looks like it is problem in your app architecture, but not in Dio.
@hongfeiyang my AuthPage now(just temporary) submits requests using
authDio
and the rest of pages usedio
. 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 placefinal 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
@YuriyBereguliak yeah I locked the first instance, but I unlock it when the second instance got 200
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
@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
@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:
@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
After reviewing your code, I see that the return statement is skipped for OnError. Try to add it.
Example
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.
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....
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);
}
}
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?
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.
I think QueuedInterceptor
can help, This issue will be closed, please trans to https://github.com/flutterchina/dio/issues/1308
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
}));
}
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
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.)
New Issue Checklist
Issue Info
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.