After switching our API Client to Combine we start to receive reports from our users about error "The operation couldn’t be completed (NSURLErrorDomain -1.)" which is the error.localizedDescription
forwarded to UI from our API client.
Top level api call looks like this:
class SomeViewModel {
private let serviceCategories: ICategoriesService
private var cancellables = [AnyCancellable]()
init(service: ICategoriesService) {
self.serviceCategories = service
}
// ...
// Yes, the block is ugly. We are only on the half way of the migration to Combine
func syncData(force: Bool = false, _ block: @escaping VoidBlock) {
serviceCategories
.fetch(force: force)
.combineLatest(syncOrders(ignoreCache: force))
.receive(on: DispatchQueue.main)
.sink { [unowned self] completion in
// bla-bla-bla
// show alert on error
}
.store(in: &cancellables)
}
}
Low level API Client call looks like:
func fetch<R>(_ type: R.Type, at endpoint: Endpoint, page: Int, force: Bool) -> AnyPublisher<R, TheError> where R : Decodable {
guard let request = request(for: endpoint, page: page, force: force) else {
return Deferred { Future { $0(.failure(TheError.Network.cantEncodeParameters)) } }.eraseToAnyPublisher()
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return URLSession.shared
.dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.background)
.tryMap { element in
guard
let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else
{ throw URLError(.badServerResponse) }
return element.data
}
.decode(type: type, decoder: decoder)
.mapError { error in
// We map error to present in UI
switch error {
case is Swift.DecodingError:
return TheError.Network.cantDecodeResponse
default:
return TheError(title: nil, description: error.localizedDescription, status: -2)
}
}
.eraseToAnyPublisher()
}
In our analytics we can clearly see chain of events:
First sought was it may be some garbage sent from backend to the client, but our server logs have records for api calls correlated to analytics logs by date and time with http status code 499.
So we can clearly determine this is not a server problem.
We also do not have reports or analytics records from users before this update.
All points to new API client switched to Combine.
It looks like session dropped by the client for some reason but at the same time it does not relates to a memory release cycle since if cancellable where released sink
closure will never be executed and alert message will not be shown.
Questions:
Notes:
I don't know for sure but I see a couple of issues in the code you presented... I commented below.
A 499 implies that your Cancellable is getting deleted before the network request completes. Maybe that will help you track it down.
Also, you don't need the subscribe(on:)
and it likely doesn't do what you think it does anyway. It could be causing the problem but there's no way to know for sure.
Using subscribe(on:)
there is like doing this:
DispatchQueue.background.async {
URLSession.shared.dataTask(with: request) { data, response, error in
<#code#>
}
}
If you understand about how URLSession works, you will see that dispatch is completely unnecessary and doesn't affect what thread the data task will emit on.
func fetch<R>(_ type: R.Type, at endpoint: Endpoint, page: Int, force: Bool) -> AnyPublisher<R, TheError> where R : Decodable {
guard let request = request(for: endpoint, page: page, force: force) else {
return Fail(error: TheError.Network.cantEncodeParameters).eraseToAnyPublisher() // your failure here is way more complex than it needs to be. A simple Fail will do what you need here.
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return URLSession.shared
.dataTaskPublisher(for: request)
// you don't need the `subscribe(on:)` here.
.tryMap { element in
guard
let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else
{ throw URLError(.badServerResponse) }
return element.data
}
.decode(type: type, decoder: decoder)
.mapError { error in
// We map error to present in UI
switch error {
case is Swift.DecodingError:
return TheError.Network.cantDecodeResponse
default:
return TheError(title: nil, description: error.localizedDescription, status: -2)
}
}
.eraseToAnyPublisher()
}