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

使用Moya/Combine实现请求重试遇问题:token刷新后未重发原请求

问题描述

使用版本:Moya/Combine (15.0.0)、CombineExt (1.8.0)

我需要在token过期时自动刷新token,成功后重新执行之前的请求,编写了如下代码:

private func request(_ target: Target) -> AnyPublisher<Moya.Response ,APIError> {
        let actualRequest = provider.requestPublisher(target)
        return actualRequest.tryMap { response in
            let json = try? response.mapJSON() as? [String: Any]
            if let json, let code = json["code"] as? String {
                if code == "ERR003" {
                    throw APIError.tokenExpired(reason: "token expired")
                }
                if code == "ERR004" {
                    throw APIError.refreshTokenExpired(reason: "login expired")
                }
            }
            return response
        }
        .mapError { error in
            if let error = error as? APIError {
                return error
            }
            if let error = error as? MoyaError {
                return APIError.moyaError(from: error)
            }
            return APIError.unknown
        }
        // `retryWhen` operator is provided by `CombineExt`
        .retryWhen { errorPublisher in
            return errorPublisher.filter { error in
                switch error {
                // When the error type is `tokenexpired`,Only go to re -request
                case .tokenExpired(_):
                    return true
                default: return false
                }
            }
            .setFailureType(to: APIError.self)
            .flatMap { error -> AnyPublisher<Void, APIError> in
                switch error {
                case .tokenExpired(_):
                    // This is my definition of re -refreshing token
                    return LogInManager.share.refreshToken()
                default:
                    return Just(()).setFailureType(to: APIError.self).mapToVoid().eraseToAnyPublisher()
                }
            }
        }
        .eraseToAnyPublisher()
       
    }

// The type of error I customized
enum APIError: Error, LocalizedError {
    case unknown, tokenExpired(reason: String), refreshTokenExpired(reason: String), moyaError(from: MoyaError)
    
    var errorDescription: String? {
        switch self {
        case .unknown:
            return "Unknown error"
        case .tokenExpired(let reason), .refreshTokenExpired(let reason):
            return reason
        case .moyaError(let from):
            return from.localizedDescription
        }
    }
}

但目前token刷新成功后,代码并未执行原请求,请问我的代码存在什么问题?


问题分析与修复

你的代码问题出在retryWhen的核心逻辑上,具体原因和修复方案如下:

核心问题

CombineExt的retryWhen需要内部返回的Publisher发送**.next事件**才会触发重试,而你当前的实现有两个关键错误:

  1. 刷新token的请求成功后,返回的Publisher只发送.finished事件,没有.next信号,retryWhen收不到重试触发指令
  2. 多余的setFailureType调用干扰了原有错误流的类型匹配
  3. 过滤后的错误流不可能进入默认分支,冗余代码完全没必要

修复后的代码

private func request(_ target: Target) -> AnyPublisher<Moya.Response ,APIError> {
    let actualRequest = provider.requestPublisher(target)
    return actualRequest.tryMap { response in
        let json = try? response.mapJSON() as? [String: Any]
        if let json, let code = json["code"] as? String {
            if code == "ERR003" {
                throw APIError.tokenExpired(reason: "token expired")
            }
            if code == "ERR004" {
                throw APIError.refreshTokenExpired(reason: "login expired")
            }
        }
        return response
    }
    .mapError { error in
        if let error = error as? APIError {
            return error
        }
        if let error = error as? MoyaError {
            return APIError.moyaError(from: error)
        }
        return APIError.unknown
    }
    .retryWhen { errorPublisher in
        // 只保留token过期的错误
        errorPublisher.filter {
            if case .tokenExpired(_) = $0 {
                return true
            }
            return false
        }
        // 刷新token后发送一个next事件触发重试
        .flatMap { _ -> AnyPublisher<Void, APIError> in
            LogInManager.share.refreshToken()
                .map { _ in () } // 将刷新结果转为Void的next事件
                .eraseToAnyPublisher()
        }
    }
    .eraseToAnyPublisher()
}

额外注意事项

  • 确保LogInManager.share.refreshToken()返回的Publisher在成功时会发送一个具体值(比如刷新后的token对象),这样map { _ in () }才能生成有效的.next(())事件
  • 如果刷新token失败(比如返回.refreshTokenExpired错误),错误会直接传递出去,不会触发重试,符合业务逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 01:31:07