I am trying to implement a function that handles Network & API errors, my problem is how to emit an observable again after filterSuccessfulStatusCodes().
The main issue I have is that I am not sure if this approach is correct, after the first subscribe.
The current error I have in this code is : Extra argument 'onError' in call
func Request<T: Decodable>(_ request: APIManager) ->
Observable<Result<T>> {
provider.rx
.request(request)
.subscribe(onSuccess: { (response) in
try response.filterSuccessfulStatusCodes()
return Observable.just(response)
.subscribe { event in
switch event {
case .success:
.map(RequestResponse<T>.self)
.map { $0.result! }
.asObservable()
.map(Result.success)
.catchError { error in
return .just(.error(withMessage: "Error \(error)"))
}
case .error:
print("error")
}
}
}, onError: { (error) in
print("network request error")
}, onCompleted: {
print("network request on completed")
}).disposed(by: disposeBag)
}
struct RequestResponse<T: Decodable> {
let statusCode: Int
let statusMessage: String
let success: Bool
let result: T?
let errorBag: String?
}
enum Result<T: Decodable> {
case success(T)
case error(withMessage: String)
}
You can try something like, which converts Single to Observable then call filterSuccessfulStatusAndRedirectCodes and you can handle the errors in catchError closure
func Request<T: Decodable>(_ request: APIManager) -> Observable<Result<T>> {
self.sharedProvider.rx
.request(request)
.asObservable()
.filterSuccessfulStatusAndRedirectCodes()
.map(RequestResponse<T>.self)
.map { Result.success }
.catchError { error in
if let moyaError = error as? MoyaError {
return Objservable.error(handleNetworkError(withMoyaError: moyaError))
} else {
return Observable.error(error)
}
}
}
Related
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)
}
I need to handle the api error codes like this and throw error for some status code. But follow code shows the above error. how can i achieve this?
func login(data: [String: Any], completion: #escaping (ResponseModel<SignUpModel>?) -> Void) throws {
NetworkAdapter.request(target: .login(data: data), success: { (response) in
if let responseModel = try? JSONDecoder().decode(ResponseModel<SignUpModel>.self,from: response.data) {
switch responseModel.statusCode {
case 2000:
completion(responseModel)
case 4005:
throw ValidationError.athenticationFailure
case .none,.some:
break
}
completion(responseModel)
} else {
}
})
}
You cannot
throw ValidationError.athenticationFailure
because the request is asynchronous. What you can do is to change the completion type to Result<ResponseModel<SignUpModel>, ValidationError> to return
completion(.success(responseModel))
on success and
completion(.failure(athenticationFailure)
on failure. By the way I buy an u 😉
Update:
Meanwhile – with Swift Concurrency – you are able to throw errors using a ThrowingContinuation
func login(data: [String: Any]) async throws -> ResponseModel<SignUpModel> {
withCheckedThrowingContinuation { continuation in
NetworkAdapter.request(target: .login(data: data), success: { (response) in
do {
let responseModel = try JSONDecoder().decode(ResponseModel<SignUpModel>.self,from: response.data)
switch responseModel.statusCode {
case 2000:
continuation.resume(returning: responseModel)
case 4005:
continuation.resume(throwing: ValidationError.athenticationFailure)
case .none,.some:
continuation.resume(returning: responseModel)
}
} catch {
continuation.resume(throwing: error)
}
})
}
}
I want to use catchError for getting back my error as custom type.
At first, I want my network layer return Observable and then in ViewModel I subscribed it for .OnNext, .OnError, .OnCompleted events, But I don't know how should I handle Errors such as 4xx, 5xx network status code and then, them return my Custom Error Object!
My Login ViewModel :
func getAccessToken() {
let network = NetworkRequest()
network.logInRequest(tokenType: .guest, token: "cce577f6021608", secondKey: "09128147040", client: "cce577f6021608bc31424d209cbf5120c3683191").subscribe(onNext: { loginData in
self.token.onNext(loginData.access_token)
}, onError: { error in
print("The Error is: \(error.localizedDescription)")
}, onCompleted: {
print("Its Completed")
}).disposed(by: bag)
}
My network layer function:
class NetworkRequest: NSObject {
var rxProvider: MoyaProvider<WebServiceAPIs>
override init() {
rxProvider = MoyaProvider<WebServiceAPIs>( plugins: [ NetworkLoggerPlugin(verbose:true) ])
}
func logInRequest(tokenType: accessTokenTypeEnum, token: String, secondKey: String, client: String) -> Observable<LoginModel> {
return rxProvider.rx
.request(WebServiceAPIs.getAccessToken(tokenType: tokenType.rawValue, token: token, secondKey: secondKey, client: client))
.filterSuccessfulStatusCodes()
.catchError({ error -> Observable<NetworkError> in
return //Observable.just() => I want to return a custom network error as obserable
})
.map(LoginModel.self, atKeyPath: nil, using: JSONDecoder(), failsOnEmptyData: true).asObservable()
}
}
thanks for any help
In my experience, '.materialize()' operator is the perfect solution for handling HTTP errors.
Instead of separate events for success and error you get one single wrapper event with either success or error nested in it.
Moya returns MoyaError enum in error block which you can handle by extracting the error type using switch on MoyaError and then using statusCode to convert to NetworkError enum
func logInRequest(tokenType: accessTokenTypeEnum, token: String, secondKey: String, client: String) -> Observable<LoginModel> {
return sharedProvider.rx
.request(WebServiceAPIs.getAccessToken(tokenType: tokenType.rawValue, token: token, secondKey: secondKey, client: client))
.filterSuccessfulStatusCodes()
.catchError({ [weak self] error -> Observable<NetworkError> in
guard let strongSelf = self else { return Observable.empty() }
if let moyaError = error as? MoyaError {
let networkError = self?.createNetworkError(from: moyaError)
return Observable.error(networkError)
} else {
return Observable.error(NetworkError.somethingWentWrong(error.localizedDescription))
}
})
.map(LoginModel.self, atKeyPath: nil, using: JSONDecoder(), failsOnEmptyData: true).asObservable()
}
func createNetworkError(from moyaError: MoyaError) -> NetowrkError {
switch moyaError {
case .statusCode(let response):
return NetworkError.mapError(statusCode: response.statusCode)
case .underlying(let error, let response):
if let response = response {
return NetworkError.mapError(statusCode: response.statusCode)
} else {
if let nsError = error as? NSError {
return NetworkError.mapError(statusCode: nsError.code)
} else {
return NetworkError.notConnectedToInternet
}
}
default:
return NetworkError.somethingWentWrong("Something went wrong. Please try again.")
}
}
You can create your custom NetworkError enum like below which will map statusCode to custom NetworkError enum value. It will have errorDescription var which will return custom description to show in error view
enum NetworkError: Swift.Error {
case unauthorized
case serviceNotAvailable
case notConnectedToInternet
case somethingWentWrong(String)
static func mapError(statusCode: Int) -> NetworkError {
switch statusCode {
case 401:
return .unauthorized
case 501:
return .serviceNotAvailable
case -1009:
return .notConnectedToInternet
default:
return .somethingWentWrong("Something went wrong. Please try again.")
}
}
var errorDescription: String {
switch self {
case .unauthorized:
return "Unauthorised response from the server"
case .notConnectedToInternet:
return "Not connected to Internet"
case .serviceNotAvailable:
return "Service is not available. Try later"
case .somethingWentWrong(let errorMessage):
return errorMessage
}
}
}
In my app I am using AlamofireObjectMapper.
I want to make a method that returns an array of objects. With the help of Alamofire I made a GET request, which gives the response as responseArray.
With using void function array listOfType always has values.
But when I use non-void function that should return array of object MedicineType, array listOfType is nil.
So here is my code.
func getAll() -> [MedicineType] {
var listOfTypes: [MedicineType]?;
Alamofire.request(BASE_URL, method:.get)
.responseArray(keyPath:"value") {(response: DataResponse<[MedicineType]>) in
if let status = response.response?.statusCode {
switch(status) {
case 200:
guard response.result.isSuccess else {
//error handling
return
}
listOfTypes = response.result.value;
default:
log.error("Error", status);
}
}
}
return listOfTypes!;
}
As i said in my comment, you need to do this in closure, instead of return it, because your call for Alamofire is async so your response will be async
This is an example, you need to add your error handle
func getAll(fishedCallback:(_ medicineTypes:[MedicineType]?)->Void){
var listOfTypes: [MedicineType]?;
Alamofire.request(BASE_URL, method:.get)
.responseArray(keyPath:"value") {(response: DataResponse<[MedicineType]>) in
if let status = response.response?.statusCode {
switch(status) {
case 200:
guard response.result.isSuccess else {
//error handling
return
}
finishedCallback(response.result.value as! [MedicineType])
default:
log.error("Error", status);
finishedCallback(nil)
}
}
}
}
Use it
classObject.getAll { (arrayOfMedicines) in
debugPrint(arrayOfMedicines) //do whatever you need
}
Hope this helps
Try closure
func getAll(_ callback :(medicineTypes:[MedicineType]?) -> Void) -> Void {
Alamofire.request(BASE_URL, method:.get)
.responseArray(keyPath:"value") {(response: DataResponse<[MedicineType]>) in
if let status = response.response?.statusCode {
switch(status) {
case 200:
guard response.result.isSuccess else {
//error handling
return
}
listOfTypes = response.result.value;
callback(listOfTypes)
default:
log.error("Error", status);
callback({})
}
}
}
}
I have a quick question:
I have a network request that returns Observable<Result<String, RequestError>>, let’s call it requestToken
if this request succeeds, I want to use the String (token) to do another request that returns Observable<Result<NSDictionary, RequestError>>, let’s call it requestData
when that second request comes back, I wanna merge the token into its dictionary
in the end I wanna map from Observable<Result<String, RequestError>> to Observable<Result<NSDictionary, RequestError>>
How can I achieve that without multiple nested levels in my code?
This is what I have today:
requestToken()
.flatMap({ result -> Observable<Result<NSDictionary, RequestError>> in
switch result {
case .success(let token):
return requestData(token: token).map({ $0.map({ $0 + ["token": token] }) })
case .failure(let error):
return Observable.of(.failure(error))
}
})
Updated:
It's a detailed example, hope this may help:
enum RequestError: Error {
case unknown
}
func requestToken() -> Observable<String> {
return Observable.create { observer in
let success = true
if success {
observer.onNext("MyTokenValue")
observer.onCompleted()
} else {
observer.onError(RequestError.unknown)
}
return Disposables.create()
}
}
func requestData(token: String) -> Observable<[String: Any]> {
return Observable<[String: Any]>.create { observer in
let success = false
if success {
observer.onNext(["uid": 007])
observer.onCompleted()
} else {
observer.onError(RequestError.unknown)
}
return Disposables.create()
}
.map { (data: [String: Any]) in
var newData = data
newData["token"] = token
return newData
}
}
requestToken() // () -> Observable<String>
.flatMapLatest(requestData) // Observable<String> -> Observable<[String: Any]>
.materialize() // Observable<[String: Any]> -> Observable<Event<[String: Any]>>
.subscribe(onNext: { event in
switch event {
case .next(let dictionary):
print("onNext:", dictionary)
case .error(let error as RequestError):
print("onRequestError:", error)
case .error(let error):
print("onOtherError:", error)
case .completed:
print("onCompleted")
}
})
.disposed(by: disposeBag)
Original:
I think it's much easier to achieve it using materialize() with less extra work:
func requestToken() -> Observable<String> { return .empty() }
func requestData(token: String) -> Observable<NSDictionary> { return .empty() }
enum RequestError: Error {}
requestToken()
.flatMapLatest(requestData)
.materialize()
.subscribe(onNext: { event in
switch event {
case .next(let dictionary):
print("onNext:", dictionary)
case .error(let error as RequestError):
print("onRequestError:", error)
case .error(let error):
print("onOtherError:", error)
case .completed:
print("onCompleted")
}
})
.disposed(by: disposeBag)
Hope this may help.
If you use the built in error system, you can save yourself from having to manually pass the error along and all the switches that would entail. You can cast the error at the end.
I would do something more like this:
// this is necessary to handle adding the token to the dictionary.
extension Dictionary {
/// An immutable version of update. Returns a new dictionary containing self's values and the key/value passed in.
func updatedValue(_ value: Value, forKey key: Key) -> Dictionary<Key, Value> {
var result = self
result[key] = value
return result
}
}
// function signatures, note that they don't return Results anymore.
func requestToken() -> Observable<String> { /*...*/ }
func requestData(withToken: String) -> Observable<[String: Any]> { /*...*/ }
requestToken().flatMapLatest {
requestData(token: $0)
.map { $0.updatedValue($0, forKey: "token") }
.map { .success($0) }
}.catchError {
Observable.just(.failure($0 as! RequestError))
}
With the above, the end result would be an Observable<Result<[String: Any], RequestError>> just like in your case, but the error handling is much cleaner.
If you can't change the signatures of the two functions you are using then I would do this:
func throwError<T, U: Error>(result: Result<T, U>) throws -> T {
switch result {
case .success(let token):
return token
case .failure(let error):
throw error
}
}
requestToken().map {
try throwError(result: $0)
}.flatMapLatest {
requestData(token: $0)
.map { try throwError(result: $0) }
.map { $0.updatedValue($0, forKey: "token") }
}
.map { .success($0) }
.catchError {
Observable.just(.failure($0 as! RequestError))
}