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:
- 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;
}
}
}
-
Use the http_interceptor plugin to fetch, you have to ways to do this:
a. Instantiating a clientClient 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.
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
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....
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...
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
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 🙌🏼
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.