This is the code of AuthInterceptor which I use for triggering token refreshing. For safety (that I the interceptor of main dio instance won't attach accesstoken to refresh token api) I even use a different instance of dio for refresh token api call. I don't see what's wrong and why sometimes it causes 401 "Invalid Refresh Token" error.
class AuthInterceptor extends QueuedInterceptor {
final Dio _dio;
final FlutterSecureStorage _secureStorage;
bool _isRefreshing = false;
final List<RequestOptions> _pendingRequests = [];
final Dio _tokenDio = Dio(
BaseOptions(
baseUrl: baseURL,
headers: {
'accept': 'application/json',
'Content-Type': 'application/json',
},
),
)..interceptors.add(
LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseBody: true,
),
);
AuthInterceptor(this._dio, this._secureStorage);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final accessToken = await _secureStorage.read(key: 'accessToken');
final refreshToken = await _secureStorage.read(key: 'refreshToken');
// Ensure we're using accessToken, not refreshToken
if (accessToken != null) {
options.headers['Authorization'] = 'Bearer $accessToken';
print('✅ Authorization header set with accessToken');
} else {
print('❌ Cannot set Authorization header - accessToken is null');
}
super.onRequest(options, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
// Token expired, attempt refresh
if (_isRefreshing) {
// If already refreshing, queue this request
_pendingRequests.add(err.requestOptions);
return;
}
final refreshToken = await _secureStorage.read(key: 'refreshToken');
if (refreshToken != null) {
print("The current refresh token is : $refreshToken");
_isRefreshing = true;
try {
final newTokens = await _refreshAccessToken(refreshToken);
if (newTokens != null) {
await _secureStorage.write(
key: 'accessToken',
value: newTokens['accessToken'],
);
await _secureStorage.write(
key: 'refreshToken',
value: newTokens['refreshToken'],
);
// Retry the original request with the new access token
final requestOptions = err.requestOptions;
requestOptions.headers['Authorization'] =
'Bearer ${newTokens['accessToken']}';
final response = await _dio.fetch(requestOptions);
handler.resolve(response);
// Process all pending requests
await _processPendingRequests(newTokens['accessToken']!);
return;
}
} on DioException catch (e) {
print('🔴 Token refresh failed (DioException): $e');
await _logOut();
handler.reject(err);
return;
} catch (e) {
print('🔴 Token refresh failed: $e');
await _logOut();
} finally {
_isRefreshing = false;
}
}
await _logOut(); // No refresh token or refresh failed
handler.reject(err);
} else {
super.onError(err, handler);
}
}
Future<Map<String, String>?> _refreshAccessToken(String refreshToken) async {
final response = await _tokenDio
.get(
RefreshToken,
options: Options(headers: {'Authorization': 'Bearer $refreshToken'}),
)
.timeout(Duration(seconds: 20));
final data = response.data as Map<String, dynamic>;
print('✅ Token refreshed successfully');
print('🔑 New accessToken: ${data['accessToken']}');
print('♻️ New refreshToken: ${data['refreshToken']}');
return {
'accessToken': data['accessToken'] as String,
'refreshToken': data['refreshToken'] as String,
};
}
Future<void> _processPendingRequests(String newAccessToken) async {
print('🔄 Processing ${_pendingRequests.length} pending requests...');
for (final requestOptions in _pendingRequests) {
requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';
await _dio.fetch(requestOptions);
}
_pendingRequests.clear();
}
Future<void> _logOut() async {
await AuthService.logout();
_pendingRequests.clear();
_isRefreshing = false;
}
}