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

Ktor手动重试失败请求异常:重复执行与循环阻塞问题

Ktor 刷新Token后重试请求的问题解决

问题根源

  1. HttpRequestRetry插件自动重试:你的HttpRequestRetry配置会对所有非成功状态码(包括401/403)自动重试,导致原请求在进入刷新Token逻辑前被插件重试一次,出现两次执行的情况。
  2. 重试逻辑未正确替换原响应:在HttpResponseValidator中直接发起新请求但未替换原响应,外层仍会抛出异常,结合Retry插件的重试机制,最终导致无限循环。
  3. 无并发保护:多个请求同时遇到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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 19:34:52