Although there is not much information about how to get a new token in case of expiration using the Dio interceptor, I will try my best to help you.
What is a HttpClient Interceptor?
HttpClient interceptors aim to modify, track and verify HTTP requests and responses from and to the server.
As you can see from the scheme, the interceptor is part of the client, and is the most edge feature in our application.

Token management using Interceptors
Since interceptors are the last part of sending HTTP requests to the servers, It's a good place to handle request retries, and get new tokens in case of expiration.
Problem with your attempt
Let's talk a bit about your code, and try to break each part of it.
onRequest:
Although this part should work just fine, It's inefficient using await
to get the access token on each HTTP request. It will drastically slow your app and the duration you retrieve your HTTP response.
I recommend you create a loadAccessToken()
function that will be responsible loading your token to a cached repository, and use that cached token on every request.
Note, that I don't know what your TokenRepository.getAccessToken()
function does behind the scene. But in case it's an HTTP request, be aware that it will be intercepted too! And if you have problem to reach your server, you will get into infinite loop of trying to get your token.
As I will explain later, I'm using the flutter_secure_storage package to save new tokens securely within the app, while loading that token, to cache, on the first HTTP request.
onError:
I recommend you to use the jwt_decode package, to identify expired tokens before sending them to the server (onRequest). That way, you will retrieve new tokens only in case of expiration, and each HTTP request will be sent with verified tokens only.
Thanks to @FDuhen comment, it's still good to mention that it's important to identify and handle 401 responses (since not all 401 are related to token expiration). You can do that via your interceptor for global behavior (like redirecting your users back to the Login flow), or per request (and handle it from your ApiRepository
). For the simplicity of the example, I won't override the onError
Interceptor function.
My approach
Now let's take a look at my code. It's not perfect, but hopefully, it will help you to understand better how to handle tokens using interceptors.
First, I have created a loadAccessToken()
function as part of my tokenRepository
class, which aims to arm the token into the cache config repository.
That function works with three steps:
- Checks if the token is in cache config.
- If not, then get try to get the token from local storage (using flutter_secure_storage).
- If it does not exist locally, then try to get that token from the server.
On each step (1, 2) I verify that the token is not expired using the jwt_decoder package. If it's expired, then I get it from the server.
Each time you are getting a new token from the server, you need to save it locally using the flutter_secure_storage (we don't want to save it using shared_preferences since it's not secure). That way, your token will be saved securely, and retrieving it will be fast.
My loadAccessToken
function:
/// Tries to get accessToken from [AppConfig], localSecureStorage or Keycloak
/// servers, and update them if necessary
Future<String?> get loadAccessToken async {
// get token from cache
var accessToken = _config.accessToken;
if (accessToken != null && !tokenHasExpired(accessToken)) {
return accessToken;
}
// get token from secure storage
accessToken =
await LocalSecureStorageRepository.get(SecureStorageKeys.accessToken);
if (accessToken != null && !tokenHasExpired(accessToken)) {
// update cache
_config.accessToken = accessToken;
return accessToken;
}
// get token from Keycloak server
final keycloakTokenResponse = await _accessTokenFromKeycloakServer;
accessToken = keycloakTokenResponse.accessToken;
final refreshToken = keycloakTokenResponse.refreshToken;
if (!tokenHasExpired(accessToken) && !tokenHasExpired(refreshToken)) {
// update secure storage
await Future.wait([
LocalSecureStorageRepository.update(
SecureStorageKeys.accessToken,
accessToken,
),
LocalSecureStorageRepository.update(
SecureStorageKeys.refreshToken,
refreshToken,
)
]);
// update cache
_config.accessToken = accessToken;
return accessToken;
}
return null;
}
And here is the tokenHasExpired
function (using the jwt_decoder package):
bool tokenHasExpired(String? token) {
if (token == null) return true;
return Jwt.isExpired(token);
}
Now, it will be much easier to handle access tokens using our interceptor. As you can see below (in my interceptor example), I'm passing a singleton AppConfig
instance and a tokenRepository
that contains the loadAccessToken()
function we talked about earlier.
All I'm doing on my override onRequest
function, is to
- Verify that the request should be used with an access token (not that relevant to our discussion).
- Get the token from the cache config (using the
AppConfig
instance).
- In case it does not exist on the cache (probably first HTTP request within app open), or the cached token has expired, then in both situations load a new access token using the
loadAccessToken
function (first from storage, only then from server token provider).
My interceptor:
class ApiProviderTokenInterceptor extends Interceptor {
ApiProviderTokenInterceptor(this._config, this._tokenRepository);
final AppConfig _config;
final TokenRepository _tokenRepository;
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
if (options.headers['requires-token'] == 'false') {
// if the request doesn't need token, then just continue to the next
// interceptor
options.headers.remove('requiresToken'); //remove the auxiliary header
return handler.next(options);
}
var token = _config.accessToken;
if (token == null || _tokenRepository.tokenHasExpired(token)) {
token = await _tokenRepository.loadAccessToken;
}
options.headers.addAll({'authorization': 'Bearer ${token!}'});
return handler.next(options);
}
@override
void onResponse(
Response<dynamic> response,
ResponseInterceptorHandler handler,
) {
return handler.next(response);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
// <-- here you can handle 401 response, which is not related to token expiration, globally to all requests
return handler.next(err);
}
}
In case you are getting an error on retrieving a new token from the server (on the loadAccessToken()
function). Then handle the error on the tokenRepository
and decide what to present to your client (something general like "There is a problem with our servers, please try again later", should be fine).
Bonus
You can add the built-in Dio LogInterceptor
to print every request and response (really helpful for debugging).
Or you can use the pretty_dio_logger package, to print beautiful, colored, request and response logs.
_ApiProvider(
dio
..interceptors.add(authInterceptor)
// ..interceptors.add(LogInterceptor())
..interceptors.add(
PrettyDioLogger(
requestBody: true,
requestHeader: true,
),
),
);