Ktor手动重试失败请求异常:重复执行与循环阻塞问题
Ktor 刷新Token后重试请求的问题解决
问题根源
- HttpRequestRetry插件自动重试:你的
HttpRequestRetry配置会对所有非成功状态码(包括401/403)自动重试,导致原请求在进入刷新Token逻辑前被插件重试一次,出现两次执行的情况。 - 重试逻辑未正确替换原响应:在
HttpResponseValidator中直接发起新请求但未替换原响应,外层仍会抛出异常,结合Retry插件的重试机制,最终导致无限循环。 - 无并发保护:多个请求同时遇到401时,会重复调用刷新Token接口,造成不必要的请求。
解决方案
1. 调整HttpRequestRetry配置,排除401/403的自动重试
修改HttpRequestRetry的retryIf逻辑,让插件只处理非401/403的失败请求,把认证相关的重试交给自定义逻辑处理:
install(HttpRequestRetry) { configuration.maxRetry?.let { maxRetries -> val max = maxRetries.invoke() retryOnException( maxRetries = max, retryOnTimeout = configuration.retryOnTimeout.invoke() ) // 仅重试非认证错误的失败请求 retryIf(maxRetries = max) { _, response -> !response.status.isSuccess() && response.status != HttpStatusCode.Unauthorized && response.status != HttpStatusCode.Forbidden } } exponentialDelay() modifyRequest { request.headers.append("x-retry-count", retryCount.toString()) } }
2. 改进刷新Token与重试逻辑,添加并发锁并替换原响应
首先在HttpClient所在类中添加一个互斥锁,防止并发请求重复刷新Token:
private val refreshTokenLock = Mutex()
然后修改HttpResponseValidator中401/403的处理逻辑:
HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> { if (response.request.url.fullPath.contains("refreshToken")) { userManager.logout() return@validateResponse } else { refreshTokenLock.withLock { // 检查当前token是否已经被其他请求刷新,避免重复操作 val originalToken = originCall.request.headers[HttpHeaders.Authorization]?.removePrefix("Bearer ") val currentAuthToken = authToken() if (originalToken == currentAuthToken.accessToken) { // 调用刷新Token接口 val refreshTokenResponse: RefreshTokenModel = request( RefreshToken( refreshToken = currentAuthToken.refreshToken.orEmpty(), email = currentAuthToken.workEmail.orEmpty(), deviceId = deviceId.orEmpty() ) ) // 更新本地存储的认证Token updateAuthToken(refreshTokenResponse) // 实现更新本地token的方法 } } // 构建新的请求,使用更新后的Token val newRequest = HttpRequestBuilder().apply { url(originCall.request.url) method = originCall.request.method // 复制原请求的所有Header,替换Authorization为新Token originCall.request.headers.forEach { key, value -> if (key != HttpHeaders.Authorization) header(key, value) } authToken().accessToken?.let { bearerAuth(it) } // 复制原请求的Body(注:如果原Body是一次性流,需要重新生成) originCall.request.content?.let { setBody(it) } // 添加标记,避免再次进入验证逻辑 attributes.put(ValidateMark, Unit) } // 发送新请求并替换原响应 val newResponse = originCall.client.send(newRequest) originCall.response.complete(newResponse.responseBody) return@validateResponse } }
3. 确保request函数使用最新Token
你的request函数已经从authToken()获取最新Token,更新本地Token后,后续请求会自动使用新Token,无需额外修改。
关键说明
- 互斥锁:确保同一时间只有一个请求执行刷新Token操作,避免重复调用刷新接口。
- 响应替换:通过
originCall.response.complete(newResponse.responseBody)替换原响应,让外层的body()方法能拿到重试后的正确结果,不会抛出异常。 - 排除认证错误的自动重试:避免Retry插件和自定义逻辑重复重试,解决原请求执行两次的问题。
内容的提问来源于stack exchange,提问作者Rahul Abrol
相关产品推荐
相关产品推荐

