Shei
Shei

Reputation: 126

Add Dio Interceptor to Flutter Riverpod

I'm making requests to a server but after some time the access token expires. I'm using Dio and recently came across interceptors. How do I add an interceptor to all calls and fetch access token using the refresh token when a 401 is returned. I'm storing my tokens in a Shared Preferences.Here's my code.



class AuthenticationService {
  final Dio _dio;
  final LocalDBService _prefs;

  AuthenticationService(this._dio, this._prefs);

  Future<User?> fetchUser() async {
    try {
      Tokens _tokens = await _prefs.getTokens();
      if (_tokens.accessToken == '') {
        return null;
      }
      final result = await _dio.get(
        CURRENT_USER,
        options: Options(headers: {
          "Authorization": "Bearer ${_tokens.accessToken}",
          'Accept': "application/json",
        }),
      );
      User _user = User.fromJson(result.data);
      return _user;
    } catch (e) {
      print("fetcherror");
      print(e);
      return null;
    }
  }
}

final authenticationServiceProvider = Provider.autoDispose<AuthenticationService>((ref) {
  final _prefs = ref.read(localDBProvider);
  final _dio = Dio(); // Need to add interceptors to this
  return AuthenticationService(_dio, _prefs);
});

final userProvider = FutureProvider.autoDispose<User?>((ref) {
  ref.maintainState = true;
  return ref.read(authenticationServiceProvider).fetchUser();
});

How and where do I add the interceptors. Would also want to move the Dio() instance to its own class

Upvotes: 1

Views: 5403

Answers (1)

EdwynZN
EdwynZN

Reputation: 5601

I would reccomend 2 dio instances, one for logging (authProvider) and another for the rest of your app, so you can lock one and use your authProvider with its own dio instance to refresh the token first

class AuthenticatorInterceptor extends Interceptor {
  final AuthenticationService authService;
  final Dio dioReference;

  AuthenticatorInterceptor(this.authService, this.dioReference);

  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    Tokens _tokens = await authService.token;
    /// get the token from your authService and do some logic with it:
    /// check if its still valid (expirationTime)
    /// refresh it if its not, etc.
    if (_token.refreshToken != null && _token.hasExpired) { // check somehow if the token is invalid and try to refresh it
    try {
        dioReference.lock(); // lock the current dio instance
        authService.refresh(); //try to get a new access token
        _tokens = await authService.token; //get the new token if the refresh was succesful
      } on DioError catch (error) { // there was an error refreshing, report it or do some logic
        dioReference.clear();
        return handler.reject(error);
      } finally {
        dioReference.unlock(); //unlock the instance and move on
      }
    }
    // if there was no error you can move on with your request with the new token (if any)
    options.headers
        .putIfAbsent(HttpHeaders.authorizationHeader, () => "Bearer ${_tokens.accessToken}");
    return handler.next(options);
  }

  @override
  void onError(DioError error, ErrorInterceptorHandler handler) async {
    if (error.type == DioErrorType.response &&
        error.response!.statusCode == 401) {
      // If you still receive 401 then maybe you should logout and report the user that the refresh token is invalid (maybe the server removed it)
    }
    return super.onError(error, handler);
  }
}

Now create a provider with a Dio instance that you will use with your app (this is a different instance from the one you will use with your AuthService, so you can lock it while asking for the token)

final dioProvider = Provider<Dio>((ref) {
  final authenticationService = ref.watch(authenticationServiceProvider);
  final Dio dio = Dio();

  ref.onDispose(dio.close);

  return dio
    ..interceptors.addAll([
      AuthenticatorInterceptor(authenticationService, dio), //this interceptor holds a reference of your authService and the dio instance that uses it
    ]);
}, name: 'Dio');

And now use it like this

final userProvider = FutureProvider.autoDispose<User?>((ref) {
      final dio = ref.watch(dioProvider);
      final result = await dio.get(
        CURRENT_USER, // I have no idea where you get this
      );
      User _user = User.fromJson(result.data);
      ref.maintainState = true; // the request was succesful, you can mantain the state now
      return _user;
});

Of course there is still a lot of logic in the background to do, your AuthService must have at least a token before doing request (there is no purpose to do calls if you know the user is not log) but this should give a general idea of how to proceed with an interceptor

Upvotes: 3

Related Questions