Combine框架下如何运行返回AnyPublisher<T, NetworkingError>的函数并捕获错误(含无效URL、错误APIKey场景)
解决Combine网络请求的错误捕获与URL有效性问题
先帮你梳理下当前代码里的几个核心问题:混用了同步网络请求和Combine异步流、URL构造失败没有通过Publisher抛出错误,还有loadAllMovie的返回类型设计不太符合Combine的最佳实践。下面一步步来修复:
1. 统一用Combine异步流,移除同步请求逻辑
你的getData方法用了同步的Data(contentsOf:),这会阻塞线程,完全违背了Combine异步的初衷,还和performOperation的异步逻辑冲突。先把这部分删掉,统一用Combine的dataTaskPublisher处理请求。
2. 修复URL构造的错误抛出逻辑
原来的代码里,URL构造失败只打印日志,但没有把错误传递到Combine流里。我们需要把URL构造逻辑包装进Combine流,用Fail来主动抛出错误。
3. 调整loadAllMovie的返回类型
把返回类型从Future<Movie, Error>改成AnyPublisher<Movie, NetworkingError>,这样更贴合Combine的链式调用风格,也能正确传递你自定义的NetworkingError。
完整修复后的代码
class MovieService { private let baseURL = "https://api.themoviedb.org/3/movie" private let apiKey = "594b8eb4999a8b44ad5136ee3ed1ebdb31" // 注:这个密钥看起来多了两位,可能是无效的,建议核对 private let language = "language=en-US" var anyCancellable = Set<AnyCancellable>() init() { } // 调整返回类型为AnyPublisher<Movie, NetworkingError> func loadAllMovie(categoryType: CategoryType, page: Int) -> AnyPublisher<Movie, NetworkingError> { // 拼接完整URL字符串 let urlString = baseURL + "/" + categoryType.rawValue + "?" + "api_key=" + apiKey + "&" + language + "&" + "page=" + "\(page)" // URL构造失败时,用Fail抛出错误到Combine流 guard let url = URL(string: urlString) else { return Fail(error: NetworkingError.unknown(NSError(domain: "MovieService", code: -1, userInfo: [NSLocalizedDescriptionKey: "URL格式无效"]))) .eraseToAnyPublisher() } // 链式调用performOperation并切换到主线程 return performOperation(requestUrl: URLRequest(url: url), responseType: Movie.self) .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } private func performOperation<T: Decodable>(requestUrl: URLRequest, responseType: T.Type) -> AnyPublisher<T, NetworkingError> { URLSession.shared.dataTaskPublisher(for: requestUrl) .jsonDecodingPublisher(type: T.self) } } enum NetworkingError: Error, LocalizedError { case decoding(DecodingError) case incorrectStatusCode(Int) case network(URLError) case nonHTTPResponse case unknown(Error) // 实现LocalizedError,让错误信息更友好 var errorDescription: String? { switch self { case .decoding(let error): return "数据解码失败:\(error.localizedDescription)" case .incorrectStatusCode(let code): return "请求失败,状态码:\(code)" // API密钥错误通常返回401/403,会被这里捕获 case .network(let error): return "网络连接错误:\(error.localizedDescription)" case .nonHTTPResponse: return "非HTTP格式响应" case .unknown(let error): return "未知错误:\(error.localizedDescription)" } } } // 保留你原有的扩展逻辑 extension Publisher { func mapErrorToNetworkingError() -> AnyPublisher<Output, NetworkingError> { mapError { error -> NetworkingError in switch error { case let decodingError as DecodingError: return .decoding(decodingError) case let networkingError as NetworkingError: return networkingError case let urlError as URLError: return .network(urlError) default: return .unknown(error) } } .eraseToAnyPublisher() } } extension URLSession.DataTaskPublisher { func emptyBodyResponsePublisher() -> AnyPublisher<Void, NetworkingError> { httpResponseValidator() .map { _ in Void() } .eraseToAnyPublisher() } } extension URLSession.DataTaskPublisher { func httpResponseValidator() -> AnyPublisher<Output, NetworkingError> { tryMap { data, response in guard let httpResponse = response as? HTTPURLResponse else { throw NetworkingError.nonHTTPResponse } let statusCode = httpResponse.statusCode guard (200..<300).contains(statusCode) else { throw NetworkingError.incorrectStatusCode(statusCode) } return (data, httpResponse) } .mapErrorToNetworkingError() } func httpResponseValidatorDataPublisher() -> AnyPublisher<Data, NetworkingError> { httpResponseValidator() .map(\.data) .eraseToAnyPublisher() } func jsonDecodingPublisher<T:Decodable>(type: T.Type) -> AnyPublisher<T, NetworkingError> { httpResponseValidatorDataPublisher() .decode(type: T.self, decoder: JSONDecoder()) .mapErrorToNetworkingError() } }
如何调用并捕获所有错误
现在你可以这样调用loadAllMovie,能正确捕获URL无效、API密钥错误、网络错误、解码错误等所有情况:
let movieService = MovieService() movieService.loadAllMovie(categoryType: .popular, page: 1) .sink(receiveCompletion: { completion in switch completion { case .finished: print("请求完成") case .failure(let error): print("捕获到错误:\(error.localizedDescription)") // 针对性处理API密钥无效的情况(状态码401/403) if case .incorrectStatusCode(let code) = error, [401, 403].contains(code) { print("API密钥无效,请检查你的密钥是否正确") } } }, receiveValue: { movie in print("获取到电影数据:\(movie)") }) .store(in: &movieService.anyCancellable)
关键修复点说明
- URL错误传递:用
Fail把URL构造失败的情况包装成Publisher错误,确保错误能通过Combine流传递,而不是只打印日志。 - 移除同步请求:删除
getData方法,统一用异步的dataTaskPublisher,避免线程阻塞。 - 友好错误信息:让
NetworkingError实现LocalizedError协议,错误信息更清晰,方便调试和后续给用户展示。 - API密钥错误捕获:当API密钥无效时,TMDB API会返回401/403状态码,
httpResponseValidator会捕获到incorrectStatusCode错误,你可以在sink的failure分支里针对性处理。
内容的提问来源于stack exchange,提问作者Ufuk Köşker
相关产品推荐
相关产品推荐

