Retry, log and refresh auth tokens with Dio
Table of Contents
dio by Flutterchina
diox mantainers are now the original package mantainers, so you can use the original package and follow along with this post as if nothing happened.
I’ll try to make a new post using http if I have the time. π
What is dio? π§ #
Dio is a powerful Http client for Dart, which supports Interceptors, Global configuration, FormData, Request Cancellation, File downloading, Timeout etc.
Concepts we will be covering in this post:
- Interceptors Interceptors are a way to intercept and modify http requests before they are sent to the server and to intercept and modify http responses before they are returned to the caller.
- Loggers A logger is a way to log the requests and responses to the console.
- Retries A retry is a way to retry a failed request.
- Token and Refresh Token A token is a way to authenticate a user and a refresh token is a way to refresh that token when it expires.
Setting up dio π§ #
To use dio, you need to add it to your pubspec.yaml
file:
dependencies:
dio: ^4.0.6
dio_smart_retry: ^1.3.2 # optional
pretty_dio_logger: ^1.1.1 # optional
The dio_smart_try
and pretty_dio_logger
packages are optional, but I’ll be using them in this post, it’s an easy way to log the requests and responses and to retry failed requests, but we will make our own retry interceptor for the refresh token part.
I also make use of flutter_appauth
and flutter_secure_storage
in the example found in the repo.
Making our custom dio client π #
To make a request, you need to create a Dio
instance and use the get
method to make a GET
request:
final dio = Dio();
final response = await dio.get<dynamic>(
'https://jsonplaceholder.typicode.com/todos/1',
);
print(response.data);
Let’s now create a custom Dio
client that we can use in our app:
class DioClient extends DioForNative {
DioClient({
List<Interceptor>? interceptors,
BaseOptions? options,
int timeOutInMilliseconds = 30 * 1000,
}) : super(
options ??
BaseOptions(
connectTimeout: timeOutInMilliseconds,
sendTimeout: timeOutInMilliseconds,
receiveTimeout: timeOutInMilliseconds,
),
);
}
Now we could simply use a new DioClient
instance instence of the Dio
in the example above.
Interceptors βοΈ #
Let’s now add some interceptors to our DioClient
:
class DioClient extends DioForNative {
DioClient(/*...*/) : super(/*...*/) {
this.interceptors.addAll(
[
// Whatever interceptors you want to add from the constructor
...?interceptors,
],
);
}
}
That’s just adding the interceptors passed in the constructor to the interceptors
list of the Dio
instance.
Let’s now have a look at the interceptors we will be using in this post.
Logger interceptor π #
The PrettyDioLogger
is a simple interceptor that logs the requests and responses to the console, if you don’t want to use it you can make your own logger interceptor making use of the LogInterceptor class, here I’ll show you how to add both, choose the one you prefer.
// ...
this.interceptors.addAll(
[
// ... previously added interceptors
// Optionally add network Logger interceptor only for debug mode
if (kDebugMode) ...[
// 1. PrettyDioLogger
PrettyDioLogger(
requestBody: true,
responseBody: true,
// ... other options
requestHeader: false,
responseHeader: false,
error: false,
request: false,
),
// 2. LogInterceptor
LogInterceptor(
requestBody: true,
responseBody: true,
// ... other options
requestHeader: false,
responseHeader: false,
error: false,
request: false,
logPrint: (log) {
// Customice to your liking
if (log.toString().isEmpty) return;
debugPrint('π ${log.toString()}');
},
),
],
],
);
// ...
How does it look like in the console?
PrettyDioLogger output:
LogInterceptor output:
Retry interceptor π #
The RetryInterceptor
is a simple interceptor that retries failed requests, there’s not much to it, you can pass the number of retries you want to make and the Dio
instance will retry the request that many times.
// ...
this.interceptors.addAll(
[
// ... previously added interceptors
// RetryInterceptor -- has some more options, check the docs
RetryInterceptor(dio: this, logPrint: print),
],
);
// ...
Token and Refresh Token interceptor π #
I don’t really know an out-of-the-box solution, so I’ll be making my own interceptor to handle this.
In the AuthRepository
class we will be using the flutter_appauth
package to authenticate the user and the flutter_secure_storage
package to store the tokens, you can read more about the implementation in the repo, take it as an example the repo does not have a complete implementation, it’s just an example.
The AuthRepository
class will be used to authenticate the user and to refresh the token when it expires. To do so, we will add a new InterceptorsWrapper
to the DioClient
directly inside the AuthRepository
class.
This wrapper will have two main methods, the onRequest
method will be called before the request is sent to the server, and the onError
method will be called before the response is returned to the caller.
class AuthRepository {
AuthRepository({
required AuthProvider authProvider,
required FlutterSecureStorage flutterSecureStorage,
}) {
_authProvider = authProvider;
_secureStorage = flutterSecureStorage;
final client = authProvider.client;
client.interceptors.add(
InterceptorsWrapper(
onRequest: (request, handler) async {
},
onError: (e, handler) async {
},
),
);
}
late final AuthProvider _authProvider;
late final FlutterSecureStorage _secureStorage;
// ...
// Implementation of the signIn, checkSession, refreshToken, signOut, deleteTokens, etc...
// Please, refer to the repo for more info about the implementation
}
The onRequest
method will be used to add the Authorization
header to the request, and the onError
method will be used to refresh the token when it expires.
Let’s now have a look at the implementation of the onRequest
method:
onRequest: (request, handler) async {
// We add the accessToken to the headers if it's not null
final accessToken = await _secureStorage.read(key: _accessTokenKey);
if (accessToken != null) {
request.headers['Authorization'] = 'Bearer $accessToken';
}
debugPrint('[DIO]: Added accessToken [${accessToken != null}]');
return handler.next(request);
},
Here we basically get the accessToken
from the FlutterSecureStorage
and add it to the request headers if it’s not null, then we call the handler.next(request)
method to continue with the request.
Like this we do not have to add the Authorization
header to every request we make, we just have to add the DioClient
instance to the AuthProvider
class and we are good to go π
Now let’s have a look at the implementation of the onError
method:
onError: (e, handler) async {
// If the statuscode is 401 we try to refresh the token
if (e.response?.statusCode == 401) {
// We refresh the token
await refreshTokens();
// We add the accessToken to the headers if it's not null
final accessToken = await _secureStorage.read(
key: _accessTokenKey,
);
if (accessToken != null) {
debugPrint('[DIO]: Refreshed Tokens');
e.requestOptions.headers['Authorization'] = 'Bearer $accessToken';
// Create request with new access token
final opts = Options(
method: e.requestOptions.method,
headers: e.requestOptions.headers,
);
final cloneReq = await client.request<void>(
e.requestOptions.path,
options: opts,
data: e.requestOptions.data,
queryParameters: e.requestOptions.queryParameters,
);
return handler.resolve(cloneReq);
}
debugPrint("[DIO]: Couldn't refresh Tokens");
}
},
Here we check if the status code of the response is 401, which, for our case, means that the token has expired, then we call the refreshTokens
method to refresh the token, and we add the new accessToken
to the request headers, then we create a new request with the new accessToken
and we return the response to the caller.
VoilΓ , now we have a working interceptor that not only refreshes the token when it expires, but also adds the accessToken
to the request headers, so we don’t have to π
Conclusion π #
In this post, I introduced you to the Dio
package, I showed you how to use it to make HTTP requests, and I showed you how to use interceptors to add custom logic to the requests.
Take this as an introduction to the Dio
package, there’s a lot more to it, you can check the docs for more info.
I hope you enjoyed it and that you found it useful.
If you have any questions or suggestions, feel free to leave a comment below. π
Thanks for reading! π€
The full source code with for this post is available here π
The pubspec.yaml file for this project uses the following dependencies π¦
dependencies:
dio: ^4.0.6 # To make HTTP requests and to use the interceptors I created
dio_smart_retry: ^1.3.2 # Easy way to add retry logic to the requests
flutter:
sdk: flutter
flutter_appauth: ^4.2.1 # Used to authenticate the user and to refresh the token
flutter_secure_storage: ^6.0.0 # Used to store the tokens in the secure storage
pretty_dio_logger: ^1.1.1 # For logging the requests
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^0.3.0 # For mocking
very_good_analysis: ^3.1.0 # Used to enforce very good practices π¦