dio_auth_interceptor 0.1.0
dio_auth_interceptor: ^0.1.0 copied to clipboard
Drop-in Dio & http interceptor for JWT refresh with a tested single-flight queue: concurrent 401s trigger exactly one refresh call, zero boilerplate.
dio_auth_interceptor #
Drop-in Dio and http interceptor for JWT access-token refresh — with a tested single-flight queue: a burst of concurrent 401s triggers exactly one refresh call, not one per request.
You provide refreshToken() and, optionally, onRefreshFailed(). Everything
else — detecting the 401, queuing concurrent requests behind the first
refresh, retrying each one with the new token, and handling a failed refresh
— is handled for you.
Why this instead of rolling your own? #
The naive version of this (a bool isRefreshing flag and a list of pending
requests) has a well-known race condition: two requests can both read
isRefreshing == false before either sets it to true, and you end up
refreshing twice. AuthRefreshController, the engine behind both adapters
below, closes that gap with a single in-flight Future — see
test/auth_refresh_controller_test.dart
for the test that proves 10 concurrent calls produce exactly 1 network call.
Usage with Dio #
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
// A plain Dio with no DioAuthInterceptor of its own, used only to call the
// refresh endpoint — this is what stops the refresh call from recursing
// into itself.
final plainDio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
dio.interceptors.add(
DioAuthInterceptor(
refreshToken: () async {
final response = await plainDio.post('/auth/refresh', data: {
'refreshToken': await secureStorage.read('refresh_token'),
});
final accessToken = response.data['accessToken'] as String;
await secureStorage.write('access_token', accessToken);
return accessToken;
},
onRefreshFailed: (error, stackTrace) => authController.logOut(),
),
);
Usage with http #
final client = AuthHttpClient(
inner: http.Client(),
refreshToken: () async {
final response = await plainClient.post(
Uri.parse('https://api.example.com/auth/refresh'),
body: {'refreshToken': await secureStorage.read('refresh_token')},
);
final accessToken = jsonDecode(response.body)['accessToken'] as String;
await secureStorage.write('access_token', accessToken);
return accessToken;
},
onRefreshFailed: (error, stackTrace) => authController.logOut(),
);
Retry only works for http.Request — the type used internally by get /
post / put / patch / delete. A streamed request (a chunked upload or
a multipart file) can't be safely re-sent after its body has already been
read once, so it's returned unchanged on a 401 instead of being retried.
Configuration #
Both DioAuthInterceptor and AuthHttpClient accept:
| Parameter | Default | Purpose |
|---|---|---|
refreshToken |
(required) | Fetches (and should persist) a new access token. Throw to signal failure. |
authHeaderBuilder |
Authorization: Bearer <token> |
Customize the header(s) applied on retry. |
shouldRefresh |
matches HTTP 401 | Decide which failures trigger a refresh. |
onRefreshFailed |
— | Called at most once per failed refresh, even if several requests were queued behind it. Good place to log out. |
Each failed request is retried at most once — if the retried request is still rejected, the original error is returned as-is rather than refreshing again, so a server that keeps returning 401 can't cause an infinite loop.
Try it end-to-end #
example/main.dart spins up a real local HTTP server (not a mock), fires 10
concurrent requests with a stale token through DioAuthInterceptor, and
prints how many times the refresh endpoint was actually hit:
dart run example/main.dart
Mock server listening on http://127.0.0.1:xxxxx
Firing 10 concurrent requests with a stale access token...
10/10 requests succeeded after a single transparent refresh.
Total refreshToken() calls: 1 (expected: 1)
What this package doesn't do #
- Token storage — pass your own
refreshToken/persistence logic; this package only coordinates when to call it. - Retry backoff/delay strategies for a failing refresh endpoint.
- Cookie-based (as opposed to bearer-token) auth flows.