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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 14:53:11