DEV3 Technologies home

DEV3 Developer Forum

How do I refresh an expired access token automatically with Dio interceptors?

Flutter helpSolved0 views
dev3 ·

My API returns 401 when the access token expires. I want Dio to refresh it once, retry the original request and not fire ten refresh calls when ten requests fail together. How do I structure the interceptor?

#flutter#dio#auth#http

0

1 reply

dev3 · Accepted answer

Approach

Use an InterceptorsWrapper (or QueuedInterceptor) that attaches the token on request and, on a 401, refreshes once and replays the request. A QueuedInterceptor processes errors one at a time, which is the simplest way to stop ten simultaneous refreshes.

class AuthInterceptor extends QueuedInterceptor {
  AuthInterceptor(this.dio, this.tokens);
  final Dio dio;          // the main client
  final TokenStore tokens;

  @override
  void onRequest(RequestOptions o, RequestInterceptorHandler h) {
    final t = tokens.access;
    if (t != null) o.headers['Authorization'] = 'Bearer $t';
    h.next(o);
  }

  @override
  Future<void> onError(DioException e, ErrorInterceptorHandler h) async {
    final isUnauthorized = e.response?.statusCode == 401;
    final alreadyRetried = e.requestOptions.extra['retried'] == true;
    if (!isUnauthorized || alreadyRetried) return h.next(e);

    try {
      // use a SEPARATE Dio (no interceptors) for the refresh and the replay
      final plain = Dio(BaseOptions(baseUrl: dio.options.baseUrl));
      final fresh = await plain.post(
        '/auth/refresh',
        data: {'refresh_token': tokens.refresh},
      );
      await tokens.save(fresh.data['access_token'], fresh.data['refresh_token']);

      final req = e.requestOptions
        ..extra['retried'] = true
        ..headers['Authorization'] = 'Bearer ${tokens.access}';
      h.resolve(await plain.fetch(req));
    } on DioException {
      await tokens.clear();   // refresh failed: force logout
      h.next(e);
    }
  }
}

Key points

  • Refresh and replay with a different Dio instance that has no auth interceptor. Calling dio.fetch from inside a QueuedInterceptor can deadlock: if the replayed request also fails, its error waits in the same queue behind the handler that is waiting for it.
  • Mark replayed requests (extra['retried']) so a second 401 does not loop.
  • Because the interceptor is queued, calls that failed while a refresh was in progress will see the new token on tokens.access; you can also skip the refresh if the stored token differs from the one used in the failed request.
  • Store tokens in flutter_secure_storage, not SharedPreferences.
  • If the refresh token is rejected, clear state and navigate to login via your router or auth notifier, not from inside the interceptor with a context.
0

Your reply

Every post is reviewed by DEV3 before it appears. Usually reviewed within 24 hours.

Markdown supported. Use ``` for code blocks. Images are not allowed.