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:
- application updated
- application opened
- main screen shown
- alert shown (NSURLErrorDomain -1)
- application backgrounded then user fall into loop "opened, alert, backgrounded" trying to restart or reinstall the app without success.
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:
- What can be wrong with this URLSession setup?
- Did you faced similar behavior and managed to solve it?
- Do you have ideas how to reproduce or at least simulate such error with URLSession?
Notes:
- We do not use SwiftUI
- iOS version vary from 14.8 to 15.0
- From 5 to 10% of users affected
- We never faced such error during development or testing