CodingAleCR / http_interceptor

A lightweight, simple plugin that allows you to intercept request and response objects and modify them if desired.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Is the interceptor design to be used without repository design pattern? Is so, any demo to how to do it?

renntbenrennt opened this issue · comments

Hi, first thanks for the great complementary package to the existing http package to address one of the most wanted feature.

However I'm having trouble to figure it out how to integrate the package into our project.

Currently, we do all the http networking in a most direct and simple way. As you can see from the two GET and POST method I pasted below:

// other codes...

Future get(String endpoint, {Map<String, String> arg}) {
  arg?.removeWhere((key, value) => key == null || value == null);

  // construct request url with Uri factory function
  final requestUrl = Uri.https(authority, endpoint, arg);
  return http.get(requestUrl);
}

Future getWithToken(String endpoint, {Map<String, String> arg}) {
  final userModel = UserModel();

  arg?.removeWhere((key, value) => key == null || value == null);

  // construct request url with Uri factory function
  final requestUrl = Uri.https(authority, endpoint, arg);

  print('requestUrl === $requestUrl');

  return http.get(
    requestUrl,
    headers: {
      HttpHeaders.authorizationHeader: 'Bearer ${userModel.token}',
    },
  );
}

Future post(String endpoint, dynamic arg) {
  final body = json.encode(arg);
  return http.post(
    '${endpoint}',
    headers: {
      HttpHeaders.contentTypeHeader: 'application/json',
    },
    body: body,
  );
}

Future postWithToken(String endpoint, dynamic arg) {
  final userModel = UserModel();
  final body = json.encode(arg);

  return http.post(
    '${endpoint}',
    headers: {
      HttpHeaders.contentTypeHeader: 'application/json',
      HttpHeaders.authorizationHeader: 'Bearer ${userModel.token}',
    },
    body: body,
  );
}

// other codes...

And this is an example of how we do networking using the function above:

import 'package:our_project/services/request.dart' as request; // this file contains the code above
import 'package:our_project/services/api/api.dart' as api; // this file contains all the api strings

// other codes...

      Response res = await request.get(api.some_api, arg: {
        'something': 123,
        'somethingElse': 'something else',
      });

     // do something with res...

// other codes...

I just basically wrap the existing http package's method http.get or http.post with my method to perform certain task such as adding header...

Now I want to use the interceptor to do the invalid-token-then-retry thing using this package. (And yes, I know that I can also use the package to do the adding the header thing, but the retry thing is top priority at the moment, so I want to achieve that first😅)

But I don't know how to start...

It seems to me that I have to rewrite the way how we do the networking... But I afraid in order to do that I have to rewrite a lot of the code since we were doing the networking in a most basic way.....

So, back to the title/question, is there a way to use the this package without using any kind of pattern... or just the most direct way to use the package is.......?

Thanks in advance!

Hey there.. I'm not the package's author nor an expert in this topic, but I've implemented interceptor and retry functionalities in my own project and want to help you out.

Please do note this implementation is from the package version 0.3.3... I assume the package would be updated soon. Implementation may change.

First just write the interceptor code;

class SampleInterceptor implements InterceptorContract {
  @override
  Future<RequestData> interceptRequest({RequestData data}) async {
    try {
      data.headers['Authorization'] = 'Bearer $token';
      data.headers["Content-Type"] = "application/json";
    } catch (e) {
      print(e);
    }
    return data;
  }

  @override
  Future<ResponseData> interceptResponse({ResponseData data}) async {
    return data;
  }
}

Next, comes retry functionality. This is implemented automatically based on the failure case we provide.

class ExpiredTokenRetryPolicy extends RetryPolicy {
  @override
  int maxRetryAttempts = 20;

  @override
  Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
    try {
      print("shouldAttemptRetryOnResponse - ${response.statusCode}");
      if (response.statusCode == 401) {
           // Do token update or your personal requirement
        return true;
      }
      return false;
    } on SocketException {
      return true;
    } catch (e) {
      print(e);
      return false;
    }
  }
}

Now we need to plug all these together.. For this I created a class containing all the api calls I would be performing.

class APIRepo {

  final contentManagementInterceptor =
      HttpClientWithInterceptor.build(interceptors: [SampleInterceptor()],retryPolicy: ExpiredTokenRetryPolicy());

Future<Response> logUserIn(String email, String password, String fcmToken,
    Map<String, dynamic> deviceInfo) async => contentManagementInterceptor.post('$apiURL/auth/login',body: jsonEncode({
             "email": "$email",
             "password": "$password",
             "fcmToken": "$fcmToken",
             "deviceInfo": deviceInfo
           }));
}

Now in any file throughout the project, we can call this api and get response data...
In somefile.dart

Response loginResponse = await APIRepo().logUserIn.(email, password);
if(loginResponse.statusCode ==200){
//Login user
}else{
//Show some error.
}

Perhaps it isn't the best implementation but it gets the work done and also you wouldn't need to rewrite most of the already implemented logic

Hope it helps.

Regards,
Sesha

Hi @SeasonLeee, you can definitely use the plugin without the repository pattern. It is designed to be agnostic to whatever pattern you desire. Here's an example on how to achieve what you are suggesting:

  1. Declare your RetryPolicy:
class YourCustomRetryPolicy extends RetryPolicy {
  @override
  int maxRetryAttempts = 5;

  @override
  Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
    try {
      if (response.statusCode == 401) {
        // Do token update or your personal requirement
        return true;
      }
      return false;
    } on SocketException {
      // Handle internet connectivity issues or return true to retry
      return true;
    } catch (e) {
      print(e);
      return false;
    }
  }
}
  1. Use the http_interceptor plugin to fetch, you have to ways to do this:
    a. Instantiating a client

     Client client = HttpClientWithInterceptor.build(interceptors: [
          WeatherApiInterceptor(),
     ]);

    Then, at the lines where you do your requests use that client.

    final response = client.get(...);

    b. Using the builder

     final http = HttpWithInterceptor.build(interceptors: [
          Logger(),
      ]);
    
     final response = http.get(...);

Let me know how it goes or if there's anything else that I can help you with.

@sesha-codeprism
@CodingAleCR

Hi super thanks for all your reply, I kind of set up the RetryPolicy in our project.

But I still have one question.

How should I stop the Retry request and eventually stop the whole request?

Well, I am doing the token refreshing, what should I do when the refresh token fail? Or I actually want the user to re-login?

Thanks in advance

Hey! Good to you're progressing... Nice! Keep going!

Now, I'm not sure we have the kind of granularity required to control an already ongoing request, but we can configure the settings to aid us quite a bit.

We can specify the number of times the failed request would be retried. I'd suggest you start by setting the retryCount to 5/10 requests.

class ExpiredTokenRetryPolicy extends RetryPolicy {

  @override
  int maxRetryAttempts = 10; // After this many attempts, the request would be dropped and has to be manually retried
  @override
  Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
      if (response.statusCode == 401) {
        refreshAccessTokens();
        return true;
      }
      return false;
}
}

For the token refresh, you can write a top-level logout function and logout user if token refresh fails. Then the user would just login as normal.

The logic would be something like this, depending on your requirements and specifications.

Future refreshAccessTokens() async {
  var token = await getSavedToken('tokens');
  UserAccessToken accessToken = UserAccessToken.fromJson(token);
  try {
    var tokenResponse = await http.post(
      Uri.parse('$apiURL/auth/refreshToken'),
      body: jsonEncode({
        "access_token": "${accessToken.accessToken}",
        "refresh_token": "${accessToken.refreshToken}"
      }),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
    );
    if (tokenResponse.statusCode != 401) {
     // Got tokens successfully, retry the request and continue normally
    } else {
     // Refresh token expired.. Log user out
      print("Unauthorized");
      clearAllSharedPrefs();  // Removing all userdata....
      logout(); //Top level function to navigate user back to login screen
    }
  } on SocketException {
 // Not connected to internet
} catch (e) {
   //Something went wrong
    print(e);
  }
}

This is my sleepy implementation 😅. You can tweak settings to your requirements.

Hope it answered your question.

Regards,
Sesha

@sesha-codeprism Thank you again for your kind reply, but before I can tackle this "stop the network request" thing.

I have a more urgent thing want to ask.

Currently the interceptor in our project is outside of the provider tree(we use package provider to do the state management), and the token is stored in the provider and sharedPreference.

During the token refreshing, besides updating the sharedPreference, I have to update the value in the provider.

And since the provider is only accessible using context(BuildContext, the one shared inside the same tree with the provider) but the interceptor is outside the tree... Is there a way to access the value of provider in the interceptor?

@CodingAleCR do you have any thoughts on this?

Thanks in advance.

My, I got a strong sense of Déjà vu here mate... because this was an issue I struggled with too during initial stages...

So here's I managed it.. I maintain all variables I would require access to without context (like this token) in a file called Globals... I can store them in providers as well, sure but sometimes I'd need to read/write/update them where I don't have context. I try to keep this list small..

class GLOBALS {
  String apiToken = "";
  String strapiToken = '';
  bool notifications = true; 
  Map<String, dynamic> userDeviceInfo;
}

While making api call, I'd append the token in interceptor

class SampleInterceptor implements InterceptorContract {
  @override
  Future<RequestData> interceptRequest({RequestData data}) async {
    try {
      data.headers['Authorization'] = 'Bearer ${GLOBALS.apiToken}';
      data.headers["Content-Type"] = "application/json";
    } catch (e) {
      print(e);
    }
    return data;
  }

As long as I do initialise it splash, I can make calls easy without provider hassle.
In SplashScreen.dart's init state

class SplashState extends State<SplashScreen> {

  @override
  void initState() {
    this.checkState()
    super.initState();
  }

void checkStore() async{
try{
var token = await getToken("strapiToken");
GLOBALS.apiToken = token.toString();
}
catch(e){
print("No token $e");
  }
}

//Rest of splash screen
}

Again, it's not the best implementation, but sure it does the trick..

If you're not okay with this GLOBALS way, you could also rely on SharedPreferences. You just need to read from SharedPreferences each time you make a call.

class SampleInterceptor implements InterceptorContract {
  @override
  Future<RequestData> interceptRequest({RequestData data}) async {
   //Getting token from Shared Preferences
   String token = await getToken("token");
    try {
      data.headers['Authorization'] = 'Bearer $token';
      data.headers["Content-Type"] = "application/json";
    } catch (e) {
      print(e);
    }
    return data;
  }

You can do this way too, but I wouldn't recommend since it would take extra time to read from shared preferences.

Hope this helps, mate.

Regards,
Sesha

@sesha-codeprism Thank you again for your kind and helpful response.

And yeah, the methods you provide sure does the trick but I wonder something more sophisticated.

Then I keep on search I found there's certain design pattern for this case.

That's when I found GetIt package: https://pub.dev/packages/get_it

But the weird thing is that even with this package, the value I retrieve with GetIt is still empty and null....

It drives me crazy.... 😅 God... really frustrating I have to admit but, well, this is also the fun of programming I think.

Although the problem is that I am not doing a personal project, I am working on our team's project and we have deadline stuff... man...

I was actually considering getting the token just use SharedPreference but in our project we have use Provider to get token in basically everywhere, and it's one thing I can get token from the SharedPreference, but it's another thing when we want to update the old token in the provider. So the question is again back to how to access the provider...

oh man...

And now I wonder why this GetIt thing is getting null value in the Interceptor.... which really doesn't make sense at all.... why........ OH MY GOD WHY.....

Hi @SeasonLeee, I had a similar problem a few months back. My problem was that the interceptor was instantiated before actually setting the token and thus the retry policy was not fetching the token correctly; it was an asynchronous messy thing between Firebase Auth, interceptors, and the actual code. I tried a few ways but in the end, what worked for me was setting the storage to both the interceptor and the retry policy with something like this:

When instantiating the interceptor/retry policy

HttpClientWithInterceptor.build(
  interceptors: [
    BearerTokenApiInterceptor(storage: _storage),
    LoggingInterceptor(),
  ],
  retryPolicy: ExpiredTokenRetryPolicy(storage: _storage),
);

BearerTokenApiInterceptor

import 'dart:io';

import 'package:http_interceptor/http_interceptor.dart';

import 'token_storage.dart';

class BearerTokenApiInterceptor implements InterceptorContract {
  final TokenStorage _storage;

  BearerTokenApiInterceptor({TokenStorage storage})
      : this._storage = storage ?? LocalTokenStorage();

  @override
  Future<RequestData> interceptRequest({RequestData data}) async {
    try {
      final token = await _storage.getToken();
      if (token != null && token.isNotEmpty) {
        print(token);
        data.headers[HttpHeaders.authorizationHeader] = "Bearer $token";
      }
    } catch (e) {
      print(e);
    }
    return data;
  }

  @override
  Future<ResponseData> interceptResponse({ResponseData data}) async => data;
}

ExpiredTokenRetryPolicy

import 'package:data/http/token_storage.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:http_interceptor/http_interceptor.dart';

class ExpiredTokenRetryPolicy extends RetryPolicy {
  final TokenStorage _storage;

  ExpiredTokenRetryPolicy({TokenStorage storage})
      : this._storage = storage ?? LocalTokenStorage();

  @override
  Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
    if (response.statusCode == 401) {
      await _updateToken();
      return true;
    }
    return false;
  }

  Future<void> _updateToken() async {
    final token = await FirebaseAuth.instance.currentUser?.getIdToken(true);

    _storage.storeToken(token);
  }
}

LocalTokenStorage

import 'package:shared_preferences/shared_preferences.dart';

abstract class TokenStorage {
  Future<String> getToken();
  Future<bool> storeToken(String token);
  Future<bool> clearToken();
}

class LocalTokenStorage extends TokenStorage {
  static const String TOKEN_KEY = "AUTH-TOKEN-API";

  Future<SharedPreferences> get storage async =>
      await SharedPreferences.getInstance();

  @override
  Future<String> getToken() async {
    final local = await storage;
    if (local.containsKey(TOKEN_KEY)) {
      return local.getString(TOKEN_KEY);
    }
    return null;
  }

  @override
  Future<bool> storeToken(String token) async {
    final local = await storage;
    return await local.setString(TOKEN_KEY, token);
  }

  @override
  Future<bool> clearToken() async {
    return await storeToken(null);
  }
}

Since you are not using the library you might have to tweak it but it's fundamentally the same. Make sure that the storage is being passed correctly to the different objects.

Now in terms of get_it and provider, there are quite a few ways that you can set those up, but the fundamentals on instantiation should not vary, just as long as you set the dependencies between classes right. I'm sorry I can't be of much help. I know it can be really frustrating sometimes.

@CodingAleCR Thank you so much for the kind and helpful response!

And I want to point out that in my last response, I said I was going to use GetIt to get the value in the Provider, but I couldn't, because I got different instant from GetIt and Provider.

Turns out, I was instantiate two instance of my data model, which leads to the situation above.

Let say, my original way of doing things are like this:

set up GetIt: (in my main.dart or where my application start):

final getIt = GetIt.instance;

getIt.registerSingleton<UserModel>(UserModel());

set up provider: (Above my APP's top-most level MaterialApp)

MultipleProvider(
    providers: [
        ChangeNotifierProvider<UserModel>(
            create: (context) => UserModel(),
        ),
    ]
 ....other codes...

And as you can see, during my set up of GetIt and Provider, I instantiate two different instance, no wonder in side the RetryPolicy class, I will get two different instance with two set of data.

I solved the two different instance problem by change the code instantiate in Provider part like this:

MultipleProvider(
    providers: [
        ChangeNotifierProvider<UserModel>(
            create: (context) => getIt.get<UserModel>(),
        ),
    ]
 ....other codes...

Basically it means, in the Provider part of instantiate the data model, I will not instantiate anything but reuse the one from GetIt instantiation/registration.

Now, I can get the data in the Provider in the RetryPolicy class, but there's still ONE HUGE problem.

Is there are ways to make the retry request slower?

Because our backend design to make the request not too fast to prevent something repeatedly taking place(for example, posting data).

But I don't see there's anything in RetryPolicy for this slow things down job...

@CodingAleCR Hey Alejandro, I have browsed through the closed issue history, and I found this closed issue #42 is pretty much my exact situation, how does @logblythe solve the problem eventually?

Also, I do not use interceptor to add token....... yeah, it's not that cool... well... yeah...

@CodingAleCR

Hey!!!! I made it!!!! In the interceptor I use a comparison like this:

      if (data.headers['Authorization'] != null) {
        if (data.headers['Authorization'] !=
            'Bearer ${getIt.get<UserModel>().token}') {
          data.headers['Authorization'] =
              'Bearer ${getIt.get<UserModel>().token}';
        }
      }

fix the issue like #42

😭 I kind of want to cry after getting this done.... anyway thank you for everything!!!!

Although I still have some other question regarding the design of the package and some possible missing feature hoping to discuss, so I keep this issue open for now, I will get back to this after I get off from work.

Thank you all again!!!

That sounds awesome!! It's great that it's working now 🥳

And sure, let me know what are your thoughts and maybe I can help out 🙌🏼

commented

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.