0

The API I'm making calls to can return JSON containing error message.

How can I tell Combine to try and decode this custom error if I'm expecting another Decodable object to be returned on successful request?

My code currently looks like this:

    private var cancellable: AnyCancellable?

    internal func perform<T>(request: URLRequest, completion: @escaping (Result<T, Error>) -> Void) where T: Decodable {
            cancellable = session.dataTaskPublisher(for: request)
                .tryMap { output in
                    guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
                        throw HTTPError.statusCode
                    }
                    return output.data
                }
                .decode(type: T.self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
                .sink(receiveCompletion: { _completion in
                    guard case .failure(let error) = _completion else {
                        return
                    }
                    completion(.failure(error))
                }, receiveValue: { value in
                    completion(.success(value))
                })
}

With URLSession I would do something like this:

URLSession.shared.dataTask(with: request) { data, response, error in
    // Check for any connection errors
    if let error = error {
        completion(.failure(error))
        return
    }
    // Read data
    guard let data = data, !data.isEmpty else {
        completion(.failure(SPTError.noDataReceivedError))
        return
    }
    // Check response's status code, if it's anything other than 200 (OK), try to decode SPTError from the data.
    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        let sptError = (try? JSONDecoder().decode(SPTError.self, from: data)) ?? SPTError.badRequest
        completion(.failure(sptError))
        return
    }
    // Decode requested objects
    do {
        let object = try JSONDecoder().decode(T.self, from: data)
        completion(.success(object))
    } catch {
        print(completion(.failure(error)))
    }
}.resume()

SPTError is just a struct that contains code and message, it conforms to Codable

Adam
  • 1,776
  • 1
  • 17
  • 28

1 Answers1

2

When you have conditional branching, you can use the .flatMap to determine which publisher to return based on whatever conditions you check.

FlatMap has to match the failure type of the upstream and the returned publisher, so need to .mapError first to a generic Error. And because different branch is a different publisher chain, type erase them all to AnyPublisher:

URLSession.shared.dataTaskPublisher(for: url)
   .mapError { $0 as Error }
   .flatMap() { output -> AnyPublisher<T, Error> in
      if output.data.isEmpty {
         return Fail(error: SPTError.noDataReceivedError).eraseToAnyPublisher()
      }
        
      guard let httpResponse = output.response as? HTTPURLResponse else {
         return Fail(error: HTTPError.statusCode).eraseToAnyPublisher()
      }
        
      if httpResponse.statusCode == 200 {
         return Just(output.data)
            .decode(type: T.self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
      } else {
         return Just(output.data)
            .decode(type: SPTError.self, decoder: JSONDecoder())
            .flatMap { Fail(error: $0) }
            .eraseToAnyPublisher()
       }
   }
   .eraseToAnyPublisher()
New Dev
  • 48,427
  • 12
  • 87
  • 129