refresh_interceptor 0.2.2 copy "refresh_interceptor: ^0.2.2" to clipboard
refresh_interceptor: ^0.2.2 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);

Session-expired widget #

Flutter apps can initialize the optional UI presenter in main and provide their own dialog widget:

Future<void> main() async {
  await RefreshInit.instance.initialize(
    sessionExpiredWidget: const SessionExpiredDialog(),
  );
  await initDI(get: GetIt.instance);
  runApp(const MyApp());
}

Use the same navigator key on MaterialApp or GetMaterialApp:

MaterialApp(
  navigatorKey: RefreshInit.instance.navigatorKey,
  // ...
)

RefreshInterceptor uses this presenter when onSessionExpired is omitted. Presenter deduplicates concurrent expiry events and does not block Dio while user interacts with dialog. Explicit callbacks remain supported.

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,
  expireSessionOnMissingToken: false,
  clearTokensOnRefreshFailure: false,
  onError: (error, stackTrace) {
    logger.error('Auth interceptor error', error, stackTrace);
  },
);

Missing tokens usually mean user is already logged out, so rejecting a request does not show session-expired UI by default. Set expireSessionOnMissingToken: true only when missing storage must be treated as an expired active session.

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() before replacing stored tokens during login, logout, or account switching. This prevents a refresh started by the previous session from overwriting the new one.

Refresh failure semantics #

Return null from onRefresh when the server permanently rejects the refresh token. The interceptor then clears tokens, according to clearTokensOnRefreshFailure, and reports session expiry.

A DioException from onRefresh that matches shouldRefresh is handled the same way. For example, refresh endpoint 401 automatically expires the session.

Throw for temporary failures such as timeouts, connection errors, and server errors. These failures are sent to onError; stored tokens remain intact and the session is not expired. The original request error continues to its caller.

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.

More documentation #

  • Package guide
  • Session API contract
  • TimelyFrontEnd integration
0
likes
0
points
338
downloads

Publisher

verified publisherpinz.dev

Weekly Downloads

Reusable Dio interceptor for attaching access tokens, refreshing them once, and retrying failed requests.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

dio, flutter

More

Packages that depend on refresh_interceptor