dio_auth_interceptor 0.1.0 copy "dio_auth_interceptor: ^0.1.0" to clipboard
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.
0
likes
160
points
79
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Drop-in Dio & http interceptor for JWT refresh with a tested single-flight queue: concurrent 401s trigger exactly one refresh call, zero boilerplate.

Repository (GitHub)
View/report issues

Topics

#dio #http #interceptor #authentication #jwt

License

MIT (license)

Dependencies

dio, http

More

Packages that depend on dio_auth_interceptor