使用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事件**才会触发重试,而你当前的实现有两个关键错误:
- 刷新token的请求成功后,返回的Publisher只发送
.finished事件,没有.next信号,retryWhen收不到重试触发指令 - 多余的
setFailureType调用干扰了原有错误流的类型匹配 - 过滤后的错误流不可能进入默认分支,冗余代码完全没必要
修复后的代码
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
相关产品推荐
相关产品推荐

