enum FailureReason : Error {
case sessionFailed(error: URLError)
case decodingFailed
case other(Error)
}
the custom error enum
private func performOperation<T: Decodable>(requestUrl: URLRequest, responseType: T.Type)->AnyPublisher<T, FailureReason>
{
return URLSession.shared.dataTaskPublisher(for: requestUrl)
.map(\.data)
.decode(type: T.self, decoder: JSONDecoder())
.mapError({ error -> FailureReason in
switch error {
case is Swift.DecodingError:
return .decodingFailed
case let urlError as URLError:
return .sessionFailed(error: urlError)
default:
return .other(error)
}
})
.eraseToAnyPublisher()
}
this is how my urlsession publisher looks like
func validateLogin(username : String , password :String) {
let url = "\(Constants.baseUrl)api/v1/auth/login/"
let htppbodyRequest = EmailLogin(username: username, password: password)
let httpBody = try! JSONEncoder().encode(htppbodyRequest)
cancellable = webservice.apiRequest(url: URL(string: url)!, resultType: User.self, httpMethodType: .post, requestBody: httpBody)
.map{ $0 }
.receive(on: RunLoop.main)
.sink(receiveCompletion: {
print("Received completion: \($0).")
}, receiveValue: { (user) in
print("user name is :\(user)")
self.subject.send(user)
})
User is the decodable struct. Even if i enter invalid username and password the Received completion of sink prints finished error is never thrown.
Subject is a passthroughSubject.
It seems you have a misunderstanding regarding when a URLSession.DataTaskPublisher should fail with an error. A data task only fails with an error in case there is a network error (such as no internet connection, SSL error, etc).
Inputting an incorrect username or password is not a network error and hence will not result in the data task throwing an error. Depending on your backend implementation, it might result in an error status code (not in the 200..<300 range) and an error response in the body of the request.
To check the status code of the HTTPURLResponse and throw an error in case it's incorrect, you can use tryMap on the dataTaskPublisher.
Here's how you can define convenience methods on URLSession.DataTaskPublisher that handle the status code of the HTTPURLResponse and throw an error in case it's incorrect.
enum NetworkingError: Error {
case decoding(DecodingError)
case incorrectStatusCode(Int)
case network(URLError)
case nonHTTPResponse
case unknown(Error)
}
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()
}
}
And then you can simplify your performOperation function as below and it will throw an error in case the status code of the response is not in the expected range.
private func performOperation<T: Decodable>(requestUrl: URLRequest, responseType: T.Type) -> AnyPublisher<T, NetworkingError> {
URLSession.shared.dataTaskPublisher(for: requestUrl)
.jsonDecodingPublisher(type: T.self)
}
Related
I am trying to figure out why the last .eraseToAnyPublisher() is giving the aforementioned error, to me it seems all the types are well specified, aren't they?
static func searchUsers(query: String) -> AnyPublisher<[UserViewModel], Never> {
// prepare URL
let urlString = "\(baseURL)/search/users?q=\(query)"
guard let url = URL(string: urlString) else {
return Just([]).eraseToAnyPublisher()
}
// get handle of native data task publisher
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
.flatMap({ users in
var userViewModels = [UserViewModel]()
users.forEach { user in
userViewModels.append(contentsOf: UserViewModel(with: user))
}
return userViewModels
})
.catch { err -> Just<[UserViewModel]> in
print(err)
return Just([])
}
.eraseToAnyPublisher() // <-- HERE IS THE ERROR
return publisher
}
Unfortunately with those complex Combine pipelines sometimes compliler errors are displayed on the wrong line. In your case there are two problems, but not where the compiler is pointing.
One being use of flatMap instead of map.
This part of the pipeline:
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
returns Publisher<[User], Error>.
Next you want to transforming that into Publisher<[UserViewModel], Error> for which you need a function map:
func map<T>(_ transform: #escaping (Output) -> T) -> Publishers.Map<Upstream, T>
which transforms one type of Output into another type of Output not flatMap:
func flatMap<T, P>(maxPublishers: Subscribers.Demand = .unlimited, _ transform: #escaping (Self.Output) -> P) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure
which transforms Output into a new Publisher
The second problem is with append(contentsOf;) which expects a Sequence of elements, in case of single elements you should use append(), but even simpler would be just to map the [User] to [UserViewModel]:
{ users in
users.map { user in
UserViewModel(with: user)
}
}
so the whole function should work with those changes:
static func searchUsers(query: String) -> AnyPublisher<[UserViewModel], Never> {
// prepare URL
let urlString = "\(baseURL)/search/users?q=\(query)"
guard let url = URL(string: urlString) else {
return Just([]).eraseToAnyPublisher()
}
// get handle of native data task publisher
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
.map { users in
users.map { user in
UserViewModel(with: user)
}
}
.catch { err -> Just<[UserViewModel]> in
print(err)
return Just([])
}
.eraseToAnyPublisher()
return publisher
}
// get handle of native data task publisher
let publisher = URLSession.shared.dataTaskPublisher(for: URL)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
return data
}
.decode(type: Repository.self, decoder: JSONDecoder())
.map { $0 }
.catch { err in
return Just([])
}
.eraseToAnyPublisher()
return publisher
I am new to Combine and I canĀ“t figure out what should I put inside the .map{} closure in order to return an array of Repository objects. The error I get at compile time is: Cannot convert value of type 'Repository' to closure result type '[Any]'
P.S. return type here should be:
-> AnyPublisher<[Repository], Never>
Can anyone share a light here? Many thanks in advance.
It was a silly mistake on my side, as Joakim Danielson pointed out, it was enought to change return type to an array of Repositories and publisher was working fine, final version of it is the following:
static func fetchRepositories(urlString: String) -> AnyPublisher<[Repository], Never> {
// prepare URL
guard let URL = URL(string: urlString) else {
return Just([]).eraseToAnyPublisher()
}
// get handle of native data task publisher
let publisher = URLSession.shared.dataTaskPublisher(for: URL)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
return data
}
.decode(type: [Repository].self, decoder: JSONDecoder())
.map { $0 }
.catch { err in
return Just([])
}
.eraseToAnyPublisher()
return publisher
}
I'm building a network API.
I'm new to Combine and I'm having some troubles with it, I'm trying to chain publish network requests, in this case I'm forming an URLRequest publisher and dispatching it on another publisher, the problem is that I cant make the flatMap work on the second publisher.
First I assemble the URLRequest with the Auth token:
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
return Deferred {
Future<URLRequest, NetworkRequestError> { promise in
if var urlComponents = URLComponents(string: baseURL) {
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
if let finalURL = urlComponents.url {
if let user = Auth.auth().currentUser {
print("##### final url -> \(finalURL)")
// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
user.getIDToken(completion: { (token, error) in
if let fbToken = token {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(headers ?? [:], uniquingKeysWith: { (first, _) in first })
print("##### API TOKEN() SUCCESS: \(defaultHeaders)")
promise(.success(request))
}
if let fbError = error {
print("##### API TOKEN() ERROR: \(fbError)")
promise(.failure(NetworkRequestError.decodingError))
}
})
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
} else {
promise(.failure(NetworkRequestError.decodingError))
}
}
}.eraseToAnyPublisher()
}
Then I'm trying to dispatch a request (publisher) and return another publisher, the problem is that the .flatMap is not getting called:
struct APIClient {
var baseURL: String!
var networkDispatcher: NetworkDispatcher!
init(baseURL: String,
networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.baseURL = baseURL
self.networkDispatcher = networkDispatcher
}
/// Dispatches a Request and returns a publisher
/// - Parameter request: Request to Dispatch
/// - Returns: A publisher containing decoded data or an error
func dispatch<R: Request>(_ request: R) -> AnyPublisher<R.ReturnType, NetworkRequestError> {
print("##### --------> \(request)")
//typealias RequestPublisher = AnyPublisher<R.ReturnType, NetworkRequestError>
return request.asURLRequest(baseURL: baseURL)
.flatMap { request in
//NOT GETTING CALLED
self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
The final publisher that is not being called is the following:
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
/// Dispatches an URLRequest and returns a publisher
/// - Parameter request: URLRequest
/// - Returns: A publisher with the provided decoded data or an error
func dispatch<ReturnType: Codable>(request: URLRequest) -> AnyPublisher<ReturnType, NetworkRequestError> {
return urlSession
.dataTaskPublisher(for: request)
// Map on Request response
.tryMap({ data, response in
// If the response is invalid, throw an error
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
// Decode data using our ReturnType
.decode(type: ReturnType.self, decoder: JSONDecoder())
// Handle any decoding errors
.mapError { error in
handleError(error)
}
// And finally, expose our publisher
.eraseToAnyPublisher()
}
}
Running the code:
struct ReadUser: Request {
typealias ReturnType = UserData
var path: String
var method: HTTPMethod = .get
init(_ id: String) {
path = "users/\(id)"
}
}
let apiClient = APIClient(baseURL: BASE_URL)
var cancellables = [AnyCancellable]()
apiClient.dispatch(ReadUser(Auth.auth().currentUser?.uid ?? ""))
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
})
.store(in: &cancellables)
I took your code and boiled it down to just the Combine parts. I could not reproduce the issue you are describing. I'll post that code below. I recommend you start simplifying your code a bit at a time to see if that helps. Factoring out the Auth and Facebook token code seems like a good candidate to start with. Another good debugging technique might be to put in more explicit type declarations to make sure your closures are taking and returning what you expect. (just the other day I had a map that I thought I was applying to an Array when I was really mapping over Optional).
Here's the playground:
import UIKit
import Combine
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, Error> {
return Deferred {
Future<URLRequest, Error> { promise in
promise(.success(URLRequest(url: URL(string: "https://www.apple.com")!)))
}
}.eraseToAnyPublisher()
}
struct APIClient {
var networkDispatcher: NetworkDispatcher!
init(networkDispatcher: NetworkDispatcher = NetworkDispatcher()) {
self.networkDispatcher = networkDispatcher
}
func dispatch() -> AnyPublisher<Data, Error> {
return asURLRequest(baseURL: "Boo!")
.flatMap { (request: URLRequest) -> AnyPublisher<Data, Error> in
print("Request Received. \(String(describing: request))")
return self.networkDispatcher.dispatch(request: request)
}.eraseToAnyPublisher()
}
}
func httpError(_ code: Int) -> Error {
return NSError(domain: "Bad Things", code: -1, userInfo: nil)
}
func handleError(_ error: Error) -> Error {
debugPrint(error)
return error
}
struct NetworkDispatcher {
let urlSession: URLSession!
public init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func dispatch(request: URLRequest) -> AnyPublisher<Data, Error> {
return urlSession
.dataTaskPublisher(for: request)
.tryMap({ data, response in
if let response = response as? HTTPURLResponse,
!(200...299).contains(response.statusCode) {
throw httpError(response.statusCode)
}
// Return Response data
return data
})
.mapError { error in
handleError(error)
}
.eraseToAnyPublisher()
}
}
let apiClient = APIClient()
var cancellables = [AnyCancellable]()
apiClient.dispatch()
.print()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { result in
debugPrint(result)
switch result {
case .failure(let error):
// Handle API response errors here (WKNetworkRequestError)
print("##### Error loading data: \(error)")
default: break
}
},
receiveValue: { value in
debugPrint(value)
})
.store(in: &cancellables)
I refactored your code. Breaking down the offending method into several functions. I could not find any problem. Below is my refactoring. You will notice that I broke all the code that constructs things into their own functions so they can be easily tested without dealing with the effect (I don't even have to mock the effect to test the logic.)
extension Request {
func asURLRequest(baseURL: String) -> AnyPublisher<URLRequest, NetworkRequestError> {
guard let user = Auth.auth().currentUser else {
return Fail(error: NetworkRequestError.missingUser)
.eraseToAnyPublisher()
}
return user.idTokenPublisher()
.catch { error in
Fail(error: NetworkRequestError.badToken(error))
}
.tryMap { token in
makeRequest(
finalURL: try finalURL(baseURL: baseURL),
fbToken: token
)
}
.eraseToAnyPublisher()
}
func finalURL(baseURL: String) throws -> URL {
guard var urlComponents = URLComponents(string: baseURL) else {
throw NetworkRequestError.malformedURLComponents
}
urlComponents.path = "\(urlComponents.path)\(path)"
urlComponents.queryItems = queryItemsFrom(params: queryParams)
guard let result = urlComponents.url else {
throw NetworkRequestError.malformedURLComponents
}
return result
}
func makeRequest(finalURL: URL, fbToken: String) -> URLRequest {
var request = URLRequest(url: finalURL)
request.httpMethod = method.rawValue
request.httpBody = requestBodyFrom(params: body)
let defaultHeaders: HTTPHeaders = [
HTTPHeaderField.contentType.rawValue: contentType.rawValue,
HTTPHeaderField.acceptType.rawValue: contentType.rawValue,
HTTPHeaderField.authentication.rawValue: fbToken
]
request.allHTTPHeaderFields = defaultHeaders.merging(
headers ?? [:],
uniquingKeysWith: { (first, _) in first }
)
return request
}
}
extension User {
func idTokenPublisher() -> AnyPublisher<String, Error> {
Deferred {
Future { promise in
getIDToken(completion: { token, error in
if let token = token {
promise(.success(token))
}
else {
promise(.failure(error ?? UnknownError()))
}
})
}
}
.eraseToAnyPublisher()
}
}
struct UnknownError: Error { }
I have a POST request to send a text message to the user that doesn't return anything, but the status code. I try to return AnyPublisher<Void, CustomError> but it won't work.
enter image description here
This is what my generic request method looks like:
func request<T>(_ req: NetworkRequest) -> AnyPublisher<T, NetworkError> where T: Decodable, T: Encodable {
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = TimeInterval(req.requestTimeout ?? requestTimeout)
guard let url = URL(string: req.url) else {
// Return a fail publisher if the url is invalid
return AnyPublisher(
Fail<T, NetworkError>(error: NetworkError.badURL("Invalid Url"))
)
}
// We use the dataTaskPublisher from the URLSession which gives us a publisher to play around with.
return URLSession.shared
.dataTaskPublisher(for: req.buildURLRequest(with: url))
.tryMap { output in
// throw an error if response is nil
guard output.response is HTTPURLResponse else {
throw NetworkError.serverError(code: 0, error: "Server error")
}
return output.data
}
.decode(type: T.self, decoder: JSONDecoder())
.mapError { error in
// return error if json decoding fails
NetworkError.invalidJSON(String(describing: error))
}
.eraseToAnyPublisher()
}
Looks like the primary issue is that you're trying to turn AnyPublisher<T,NetworkError> into AnyPublisher<Void,NetworkError>.
You could do:
service.request(request).map { _ in () }.eraseToAnyPublisher()
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
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()