refresh_interceptor 0.1.0
refresh_interceptor: ^0.1.0 copied to clipboard
Reusable Dio interceptor for attaching access tokens, refreshing them once, and retrying failed requests.
refresh_interceptor #
Shared token refresh for Dio. One setup works with one or many authenticated Dio clients.
What it handles #
- Adds access tokens to requests.
- Shares one refresh across concurrent requests and Dio clients.
- Retries each failed request once with newest token.
- Supports refresh-token rotation.
- Prevents refresh loops.
- Emits session expiry once per login session.
- Detects a newly stored login token automatically.
- Supports custom auth schemes, expiry rules, and public routes.
- Has no service-locator or state-management dependency.
Setup #
Use TokenStoreAdapter to connect existing storage methods. No extra adapter
class needed:
final tokenStore = TokenStoreAdapter(
readAccessToken: authLocal.getAccessToken,
readRefreshToken: authLocal.getRefreshToken,
saveTokens: authLocal.updateTokens,
clearTokens: authLocal.clearTokens,
);
Create one RefreshInterceptor for the authenticated session. Use a separate
Dio client for refresh calls so refresh never intercepts itself:
final apiDio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
final refreshDio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
final auth = RefreshInterceptor(
tokenStore: tokenStore,
onRefresh: (refreshToken) async {
final response = await refreshDio.post<Map<String, dynamic>>(
'/authorization/token/refresh/',
data: {'refresh': refreshToken},
);
final data = response.data!;
return RefreshTokens(
accessToken: data['access'] as String,
refreshToken: data['refresh'] as String?,
);
},
onSessionExpired: () {
// Reset app state or navigate to login.
},
);
auth.attachTo(apiDio);
For multiple authenticated clients, attach the same instance:
auth.attachToAll([
appDio,
insuranceDio,
notificationDio,
]);
All clients now share one in-flight refresh. This matters when servers rotate refresh tokens.
Existing token repositories #
TokenStoreAdapter accepts method tear-offs matching this common API:
Future<String?> getAccessToken();
Future<String?> getRefreshToken();
Future<void> updateTokens(String accessToken, String? refreshToken);
Future<void> clearTokens();
You can also implement TokenStore directly.
Configuration #
Default header is Authorization: Bearer <token>. Change scheme:
final auth = RefreshInterceptor(
// ...
tokenPrefix: 'Token',
);
Customize protected routes and expiry detection:
final auth = RefreshInterceptor(
// ...
shouldAttachToken: (request) => !request.path.startsWith('/public/'),
shouldRefresh: (error) {
final data = error.response?.data;
return error.response?.statusCode == 401 ||
data is Map && data['code'] == 'token_expired';
},
rejectIfTokenMissing: true,
clearTokensOnRefreshFailure: false,
onError: (error, stackTrace) {
logger.error('Auth interceptor error', error, stackTrace);
},
);
Session lifecycle #
Session-expiry callback fires once for concurrent failures. When a new access
token appears after login, expiry state resets automatically. Call
auth.resetSession() only when an app reuses exactly the same token value for a
new session.
Proactive refresh and detach #
final success = await auth.refreshAccessToken();
auth.detachFrom(apiDio);
Proactive failure returns false without expiring session.
Retry note #
Dio request bodies backed by one-shot streams cannot always be replayed. Buffer upload data when requests must survive token refresh.
See example/ for runnable Android, iOS, and web Flutter app.