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

使用Refresh Token刷新Bearer Token时HttpClient请求异常问题

解决Token刷新时的TaskCancelletionException问题及并发请求处理

你的问题核心出在两个地方:一是httpClient.CancelPendingRequests()会取消所有挂起请求(包括你刚要发起的刷新Token请求),直接导致了取消异常;二是没有处理并发场景下多个请求同时触发Token刷新的冲突。下面是具体的解决方案:

1. 核心修复思路

  • 移除全局取消请求的操作,避免刷新请求被误取消
  • 用异步锁保证同一时间只有一个刷新Token的操作在执行
  • 优化重试逻辑,确保刷新成功后用新Token正确重试原请求

2. 具体代码实现

第一步:添加异步锁字段

在你的请求处理类中添加一个异步锁,用于控制并发刷新:

// 异步锁,确保同一时间只有一个刷新Token操作执行
private readonly SemaphoreSlim _refreshTokenLock = new SemaphoreSlim(1, 1);

第二步:修改SendAsyncRequest方法

public async Task<HttpResponseMessage> SendAsyncRequest(string uri, string content, HttpMethod method, bool tryReauthorizeOn401 = true) {
 try {
 HttpRequestMessage rm = new HttpRequestMessage(method, uri);
 if (!string.IsNullOrWhiteSpace(content))
 rm.Content = new StringContent(content, Encoding.UTF8, ContentType);

 HttpResponseMessage response = await httpClient.SendAsync(rm);

 if (response.StatusCode == HttpStatusCode.Unauthorized && tryReauthorizeOn401) {
     // 等待刷新锁,避免并发刷新
     await _refreshTokenLock.WaitAsync();
     try {
         // 先检查Token是否已被其他请求刷新(可选:比如对比当前Token是否还是过期的那个)
         bool refreshSuccess = await OnReauthorizeUser();
         if (!refreshSuccess) return response;

         // 用新Token重发原请求,关闭重试避免循环
         return await SendAsyncRequest(uri, content, method, false);
     } finally {
         // 释放锁,让其他等待的请求可以重试
         _refreshTokenLock.Release();
     }
 }
 return response;
 }catch(TaskCanceledException ex) {
 Debug.Assert(false, $"请求被取消: {ex.Message}");
 // 可选:如果是刷新请求被意外取消,可以考虑重试一次
 throw;
 }catch(Exception ex) {
 Debug.Assert(false, ex.Message);
 throw;
 }
}

第三步:优化OnReauthorizeUser方法

确保刷新Token的请求不会触发重试逻辑,避免循环:

private async Task<bool> OnReauthorizeUser() {
 // 从本地存储获取RefreshToken
 var storedRefreshToken = GetStoredRefreshToken();
 // 调用刷新接口,关闭重试开关
 var refreshResponse = await SendAsyncRequest(
     "/tokens/refresh/",
     JsonConvert.SerializeObject(new { RefreshToken = storedRefreshToken }),
     HttpMethod.Post,
     tryReauthorizeOn401: false
 );

 if (!refreshResponse.IsSuccessStatusCode) {
     // 刷新失败,比如RefreshToken过期,这里可以跳转到登录页
     return false;
 }

 // 解析新的令牌
 var newTokens = await refreshResponse.Content.ReadAsAsync<TokenResponse>();
 // 更新本地存储的令牌
 StoreNewTokens(newTokens.BearerToken, newTokens.RefreshToken);
 // 更新HttpClient的默认Authorization头,后续请求自动携带新Token
 httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", newTokens.BearerToken);

 return true;
}

3. 流程说明

用户点击“查看我的文档”后,整个流程会变成:

  1. 发送/documents/user/user-id请求,收到401响应
  2. 进入刷新逻辑,等待异步锁(如果有其他并发请求也会在这里排队)
  3. 调用OnReauthorizeUser获取新的BearerToken,更新本地存储和HttpClient的请求头
  4. 释放异步锁,重发原请求
  5. 收到成功响应,展示文档给用户

额外优化建议

  • 避免重复刷新:在进入刷新逻辑前,可以先检查当前的BearerToken是否已经被其他请求刷新过,若已更新则直接重试原请求
  • 失败处理:如果RefreshToken也过期,应直接跳转到登录页面,避免用户看到无意义的错误
  • 精准取消:如果需要取消请求,不要用全局的CancelPendingRequests,而是为每个请求单独使用CancellationToken,精准控制取消范围

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 11:42:40