dio_auth_guard 0.0.1
dio_auth_guard: ^0.0.1 copied to clipboard
Automatically handles 401/403 responses in Dio — cancels all in-flight requests and navigates to a custom unauthenticated screen. Router-agnostic.
dio_auth_guard #
A Flutter package that automatically handles 401/403 responses by cancelling all in-flight Dio requests and navigating to a custom unauthenticated screen.
Features #
- Automatic 401/403 detection via a Dio interceptor — no manual status-code checking needed
- Cancels ALL in-flight requests the moment a session expires, preventing stale responses from resolving after redirect
- Debounced — when multiple simultaneous requests all return 401, only one redirect is triggered
- Router-agnostic — works with GoRouter, AutoRoute, plain Navigator, or any routing solution
- State management agnostic — works with Bloc, Cubit, Riverpod, GetX, Provider, or none at all
- Single shared
CancelToken— one token passed to every request, cancelled atomically on auth failure onLoginSuccess()andonLogout()hooks — re-arms the guard after re-authentication or explicit sign-out
Installation #
Add to your pubspec.yaml:
dependencies:
dio_auth_guard: ^0.0.1
Then run:
flutter pub get
Setup #
Call AuthGuardConfig.init() and AuthGuardModule.register() once in main(), before runApp.
Option A — GoRouter (recommended for declarative routing) #
Use onUnauthenticated when your app uses GoRouter, AutoRoute, or any other declarative router. The callback hands navigation control back to your router so it can manage its own stack.
import 'package:dio/dio.dart';
import 'package:dio_auth_guard/dio_auth_guard.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final navigatorKey = GlobalKey<NavigatorState>();
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
final router = GoRouter(
navigatorKey: navigatorKey,
initialLocation: '/home',
routes: [
GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
],
);
void main() {
AuthGuardConfig.init(
navigatorKey: navigatorKey,
onUnauthenticated: () {
navigatorKey.currentContext?.go('/login');
},
);
AuthGuardModule.register(dio);
runApp(MaterialApp.router(routerConfig: router));
}
Option B — Plain Navigator #
Use unauthWidget when your app uses Flutter's built-in Navigator. The guard calls pushAndRemoveUntil, replacing the entire navigation stack with the provided widget so the user cannot navigate back to a protected screen.
import 'package:dio/dio.dart';
import 'package:dio_auth_guard/dio_auth_guard.dart';
import 'package:flutter/material.dart';
final navigatorKey = GlobalKey<NavigatorState>();
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
void main() {
AuthGuardConfig.init(
navigatorKey: navigatorKey,
unauthWidget: const LoginScreen(),
);
AuthGuardModule.register(dio);
runApp(
MaterialApp(
navigatorKey: navigatorKey,
home: const HomeScreen(),
),
);
}
Usage #
1. Pass AuthGuard.token to every Dio request #
AuthGuard.token is the shared CancelToken that the guard cancels atomically when a 401/403 is received. Pass it to every request so they can all be stopped simultaneously:
Future<Response> getProfile() {
return dio.get(
'/profile',
cancelToken: AuthGuard.token,
);
}
If you use a Retrofit-style service class, pass it in every generated method call or wire it up in a request interceptor.
2. Call AuthGuard.onLoginSuccess() after a successful login #
The guard debounces repeated 401s using an internal _isHandling flag. After the user logs in again you must reset this flag, otherwise the guard will silently ignore all future 401/403 responses.
Future<void> login(String email, String password) async {
final response = await dio.post('/auth/login', data: {
'email': email,
'password': password,
});
// Store tokens...
await secureStorage.write(key: 'access_token', value: response.data['token']);
// Re-arm the guard before navigating into the app.
AuthGuard.onLoginSuccess();
router.go('/home');
}
3. Call AuthGuard.onLogout() on explicit sign-out #
onLogout() resets _isHandling and issues a fresh CancelToken. Call it whenever the user logs out intentionally so the guard is clean for the next session.
Future<void> logout() async {
await secureStorage.deleteAll();
// Reset the guard state and cancel token.
AuthGuard.onLogout();
router.go('/login');
}
4. The _safeRequest catch block (important) #
Some Dio setups use validateStatus: (_) => true or catch DioExceptionType.badResponse directly in a wrapper function. In these cases Dio treats 4xx responses as successful responses rather than exceptions, so AuthInterceptor.onError is never called.
If your project uses this pattern, add an explicit check inside your wrapper:
Future<T?> _safeRequest<T>(Future<T> Function() request) async {
try {
return await request();
} on DioException catch (e) {
if (e.type == DioExceptionType.badResponse) {
final code = e.response?.statusCode;
if (code == 401 || code == 403) {
AuthGuard.instance.handleUnauthenticated();
return null;
}
}
rethrow;
}
}
API Reference #
| Member | Type | Description |
|---|---|---|
AuthGuardConfig.init() |
static void |
One-time setup. Provide navigatorKey and either unauthWidget or onUnauthenticated. |
AuthGuardModule.register(dio) |
static void |
Attaches AuthInterceptor to the given Dio instance. Safe to call multiple times. |
AuthGuardModule.unregister(dio) |
static void |
Removes AuthInterceptor from the given Dio instance. Useful in tests. |
AuthGuard.instance |
AuthGuard |
The singleton instance. |
AuthGuard.token |
static CancelToken |
The current shared cancel token. Pass to every Dio request. |
AuthGuard.instance.handleUnauthenticated() |
void |
Cancels in-flight requests and triggers navigation. Debounced. |
AuthGuard.onLoginSuccess() |
static void |
Resets _isHandling after a successful login. |
AuthGuard.onLogout() |
static void |
Resets _isHandling and issues a fresh CancelToken on explicit logout. |
How it works #
Request A ──┐
Request B ──┼──► Dio ──► API server
Request C ──┘ │
│ 401 Unauthorized
▼
AuthInterceptor.onError
│
▼
AuthGuard.handleUnauthenticated()
│
┌───────────┴────────────┐
│ │
Cancel all tokens Future.delayed(0)
(Requests A, B, C navigate to login
all cancelled)
│
▼
AuthInterceptor receives
DioExceptionType.cancel
for each cancelled request
│
▼
Swallowed silently ✓
- Every Dio request shares
AuthGuard.tokenas itscancelToken. - When a 401/403 is received,
AuthInterceptor.onErrorcallshandleUnauthenticated(). - The guard immediately calls
cancelToken.cancel('session_expired'), which causes every in-flight request sharing that token to throw aDioExceptionType.cancelerror. - Those cancel errors flow back through
AuthInterceptor.onError, which recognises them and swallows them silently — they are an intentional side-effect, not real failures. - Navigation is deferred via
Future.delayed(Duration.zero)so Dio's response pipeline finishes cleanly before the widget tree is modified. - A fresh
CancelTokenis issued immediately so requests made after re-authentication are unaffected.
Common issues #
Login screen not launching after a 401 #
Cause: Your project catches DioExceptionType.badResponse in a _safeRequest wrapper before the error reaches AuthInterceptor. The interceptor's onError is never called.
Fix: Add an explicit 401/403 check in your _safeRequest wrapper as shown in the Usage section above.
Multiple simultaneous 401s cause multiple redirects #
Cause: Sounds like the guard is not yet configured, or onLoginSuccess() was called mid-flight.
Fix: This scenario is already handled by the debounce. The _isHandling flag ensures only the first 401 triggers navigation. Verify you are not calling AuthGuard.onLoginSuccess() before the login flow completes.
Requests after login still fail or get cancelled immediately #
Cause: AuthGuard.onLoginSuccess() was not called after a successful login, so _isHandling remains true and the guard silently ignores subsequent 401/403 responses. Alternatively, the old (cancelled) CancelToken is still being passed to new requests.
Fix:
- Always call
AuthGuard.onLoginSuccess()at the end of your login success handler. - Always read
AuthGuard.tokenfresh for each request — do not store the token in a variable and reuse it, as it may refer to a cancelled token from a previous session.