-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Comments
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. |
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.
works separately for every request, but not for whole bunch of requests. |
I kinda found a work around using static class. 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. looks like this package abandoned. |
First, Please make sure all multiple requests are Initiated by the same one dio instance. Then, Please check out: |
Why executing sequential is need? What is the senior ? // 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 |
Yea I think I missed the error lock, I only locked request and response |
@YuriyBereguliak , @hongfeiyang Hi, I tried 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 . 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. |
@kateile |
@hongfeiyang my AuthPage now(just temporary) submits requests using 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? Looks like it is problem in your app architecture, but not in Dio. |
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 |
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.
|
@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 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: |
Hey, |
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 Anyway. I sort of solved it by using the Queue package: https://pub.dev/packages/queue By creating a In the
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 |
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?
|
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
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]);
});
});
});
}
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 |
Regarding the need to prevent concurrent requests from trying to refresh the multiple times, especially now that the 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 |
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.
The text was updated successfully, but these errors were encountered: