I'm facing an issue with the Dio package in my Flutter app. I have implemented an interceptor to add headers to every request and to handle 401 response by simply logging out. However, the implementation seems to be behaving unexpectedly, and I'm seeking guidance to resolve the issue.
Here's a simplified version of my DioClient class:
class DioClient {
Dio dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 100),
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"Content-Type": 'application/json'},
));
TokenManager tokenManager = TokenManager();
AppPreference appPreference = AppPreference();
DioClient() {
dio.interceptors
..clear()
..add(InterceptorsWrapper(
onRequest: (options, handler) async {
final accessToken = await tokenManager.getAccessToken();
final refreshToken = await tokenManager.getRefreshToken();
options.headers['Authorization'] = 'Bearer $accessToken';
options.headers['Cookie'] = '$refreshToken';
handler.next(options);
},
onError: (DioException error, handler) async {
if (error.response?.statusCode == 401) {
dio.interceptors.clear();
final options = error.response!.requestOptions;
await tokenManager.deleteTokens();
Hive.box<User>('userBox').clear();
await appPreference.logOut();
showSessionExpiredDialog(navigatorKey.currentContext!);
return handler.reject(DioException(requestOptions: options));
}
return handler.next(error);
},
onResponse: (response, handler) async {
if (response.statusCode == 200 && response.data != null) {
Map<String, dynamic> responseData = response.data;
if (responseData.containsKey("newAccessToken")) {
final newAccessToken = responseData["newAccessToken"];
if (newAccessToken != null) {
await tokenManager.writeAccessToken(newAccessToken);
response.requestOptions.headers['Authorization'] =
'Bearer $newAccessToken';
final refreshToken = await tokenManager.getRefreshToken();
response.requestOptions.headers['Cookie'] = '$refreshToken';
return handler.resolve(response);
}
}
}
return handler.next(response);
},
));
}
}
void showSessionExpiredDialog(BuildContext context) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
alignment: Alignment.center,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Session Expired",
style: getSemiBoldStyle(color: Colors.black, fontSize: 16.sp)),
SizedBox(
height: 10.h,
),
SvgPicture.asset(
'assets/svg/security-error.svg',
height: 200.h,
),
SizedBox(
height: 10.h,
),
Text(
"Your session has expired! Please login again",
textAlign: TextAlign.center,
style: getSemiBoldStyle(
color: AppColors.blackPrimaryMinus1,
fontSize: FontSize.s14.sp),
),
],
),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () {
Navigator.of(context).pop();
navigatorKey.currentState
?.pushReplacementNamed(Routes.loginRoute);
},
),
],
);
},
);
}
Problem The issue arises when replacing the expired accesstoken with new one. On status code 401, I have implemented code to clear credentials, show a session expired dialog, and delete tokens. However, it should only appear when the refresh token expires. It appears that after replacing accesstoken with new one only few interceptor will use it, meaning old interceptor uses expired token hence resulting in unauthorized scenario.
Expected Behavior The expected behavior is as follows:
Add headers to every request (working fine). Check for a new access token in each response. If there is a new access token, replace the old one. If the error status code is 401, clear all credentials, and show a session expired dialog prompting to login again
Question How to make sure that every interceptor after replacing new access token uses that new token instead of old one? How can I ensure that interceptors are cleared after handling a 401 error and before rejecting it?
I'm facing an issue with the Dio package in my Flutter app. I have implemented an interceptor to add headers to every request and to handle 401 response by simply logging out. However, the implementation seems to be behaving unexpectedly, and I'm seeking guidance to resolve the issue.
Here's a simplified version of my DioClient class:
class DioClient {
Dio dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 100),
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"Content-Type": 'application/json'},
));
TokenManager tokenManager = TokenManager();
AppPreference appPreference = AppPreference();
DioClient() {
dio.interceptors
..clear()
..add(InterceptorsWrapper(
onRequest: (options, handler) async {
final accessToken = await tokenManager.getAccessToken();
final refreshToken = await tokenManager.getRefreshToken();
options.headers['Authorization'] = 'Bearer $accessToken';
options.headers['Cookie'] = '$refreshToken';
handler.next(options);
},
onError: (DioException error, handler) async {
if (error.response?.statusCode == 401) {
dio.interceptors.clear();
final options = error.response!.requestOptions;
await tokenManager.deleteTokens();
Hive.box<User>('userBox').clear();
await appPreference.logOut();
showSessionExpiredDialog(navigatorKey.currentContext!);
return handler.reject(DioException(requestOptions: options));
}
return handler.next(error);
},
onResponse: (response, handler) async {
if (response.statusCode == 200 && response.data != null) {
Map<String, dynamic> responseData = response.data;
if (responseData.containsKey("newAccessToken")) {
final newAccessToken = responseData["newAccessToken"];
if (newAccessToken != null) {
await tokenManager.writeAccessToken(newAccessToken);
response.requestOptions.headers['Authorization'] =
'Bearer $newAccessToken';
final refreshToken = await tokenManager.getRefreshToken();
response.requestOptions.headers['Cookie'] = '$refreshToken';
return handler.resolve(response);
}
}
}
return handler.next(response);
},
));
}
}
void showSessionExpiredDialog(BuildContext context) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
alignment: Alignment.center,
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Session Expired",
style: getSemiBoldStyle(color: Colors.black, fontSize: 16.sp)),
SizedBox(
height: 10.h,
),
SvgPicture.asset(
'assets/svg/security-error.svg',
height: 200.h,
),
SizedBox(
height: 10.h,
),
Text(
"Your session has expired! Please login again",
textAlign: TextAlign.center,
style: getSemiBoldStyle(
color: AppColors.blackPrimaryMinus1,
fontSize: FontSize.s14.sp),
),
],
),
actions: [
TextButton(
child: const Text("OK"),
onPressed: () {
Navigator.of(context).pop();
navigatorKey.currentState
?.pushReplacementNamed(Routes.loginRoute);
},
),
],
);
},
);
}
Problem The issue arises when replacing the expired accesstoken with new one. On status code 401, I have implemented code to clear credentials, show a session expired dialog, and delete tokens. However, it should only appear when the refresh token expires. It appears that after replacing accesstoken with new one only few interceptor will use it, meaning old interceptor uses expired token hence resulting in unauthorized scenario.
Expected Behavior The expected behavior is as follows:
Add headers to every request (working fine). Check for a new access token in each response. If there is a new access token, replace the old one. If the error status code is 401, clear all credentials, and show a session expired dialog prompting to login again
Question How to make sure that every interceptor after replacing new access token uses that new token instead of old one? How can I ensure that interceptors are cleared after handling a 401 error and before rejecting it?