You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Dio QueuedInterceptor处理多请求时Token刷新重复调用问题排查

Dio并发请求401重复刷新Token的问题分析与解决

问题根源

  1. 无并发同步机制:三个请求同时触发401错误,各自进入onError回调后,都会独立执行refresh_token请求,没有互斥逻辑,导致重复调用刷新接口。
  2. Token未全局生效:第一个请求刷新成功后,仅修改了自身请求的Header并重试,但未更新Dio实例的全局默认Header,另外两个请求的拦截器仍会判定Token过期,继续触发刷新。
  3. QueuedInterceptor的错误处理逻辑:队列拦截器仅保证请求的发送顺序,但错误回调是每个请求独立触发的,不会自动等待其他请求的错误处理完成。

解决方案

1. 新增全局互斥锁,避免重复刷新

使用Dart的Lock类(需导入sync包),确保同一时间只有一个请求执行Token刷新逻辑,其他请求等待锁释放后直接使用新Token重试。

2. 刷新成功后全局更新Dio的默认Header

将新获取的Token更新到Dio实例的全局headers中,后续所有请求(包括等待的请求)都会自动携带新Token。

3. 统一处理等待的401请求

刷新Token完成后,让所有等待的请求使用新Token重试,无需各自再触发刷新。

修改后的代码示例

import 'package:dio/dio.dart';
import 'package:synchronized/synchronized.dart';

class AuthInterceptor extends QueuedInterceptor {
  // 全局互斥锁,保证同一时间只有一个刷新操作
  final _lock = Lock();
  // 标记是否正在刷新Token
  bool _isRefreshing = false;
  // 存储等待刷新完成后重试的请求
  final List<ErrorInterceptorHandler> _pendingRequests = [];

  @override
  Future<void> onError(DioError err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      await _lock.synchronized(() async {
        // 若正在刷新Token,将当前请求加入等待队列
        if (_isRefreshing) {
          _pendingRequests.add(handler);
          return;
        }

        _isRefreshing = true;
        try {
          final tokenDio = Dio();
          final refreshResponse = await tokenDio.post('/${endPoint.path}',
              data: endPoint.requestBody);

          if (refreshResponse.statusCode == 201) {
            final newAccessToken = refreshResponse.data['access_token'];
            // 全局更新Dio的默认Header
            dio.options.headers['Authorization'] = 'Bearer $newAccessToken';

            // 重试当前请求
            final response = await dio.request(
              err.requestOptions.path,
              data: err.requestOptions.data,
              queryParameters: err.requestOptions.queryParameters,
              options: Options(
                method: err.requestOptions.method,
                headers: err.requestOptions.headers
                  ..['Authorization'] = 'Bearer $newAccessToken',
              ),
            );
            handler.resolve(response);

            // 重试所有等待的请求
            _processPendingRequests(newAccessToken);
          } else {
            // 刷新失败,拒绝所有请求
            handler.reject(err);
            _rejectPendingRequests(err);
          }
        } catch (error) {
          handler.reject(err);
          _rejectPendingRequests(err);
        } finally {
          _isRefreshing = false;
        }
      });
    } else {
      handler.next(err);
    }
  }

  // 处理等待的请求,用新Token重试
  void _processPendingRequests(String newToken) {
    for (final handler in _pendingRequests) {
      final options = handler.requestOptions;
      options.headers['Authorization'] = 'Bearer $newToken';
      dio.request(
        options.path,
        data: options.data,
        queryParameters: options.queryParameters,
        options: Options(method: options.method, headers: options.headers),
      ).then((response) {
        handler.resolve(response);
      }).catchError((error) {
        handler.reject(error as DioError);
      });
    }
    _pendingRequests.clear();
  }

  // 刷新失败时,拒绝所有等待的请求
  void _rejectPendingRequests(DioError err) {
    for (final handler in _pendingRequests) {
      handler.reject(err);
    }
    _pendingRequests.clear();
  }
}

关键说明

  • _lock.synchronized确保刷新Token的逻辑同一时间只有一个执行路径。
  • _isRefreshing标记避免重复进入刷新流程。
  • _pendingRequests存储所有等待Token刷新的请求,刷新完成后统一重试,避免每个请求都触发刷新。

内容的提问来源于stack exchange,提问作者hemant

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 16:07:04