How to generate and handle errors in Swift - ios

I'm working on an app where users need to login. The webapplication uses a token, that users can get from calling a webservice using their username and password. My question now is what's the best way the handle errors that can occur. What I have now:
LoginViewController.swift
self.api.token(forUsername: "a", password: "b") { (result) in
switch (result) {
case .failure(let error):
print("something when wrong \(error)")
case .success(let token):
print("token \(token)")
}
LoginAPIClient (the self.api) using https://github.com/3lvis/Networking
class LoginAPIClient {
enum ResponseError: Error {
case unexpectedDataError
case unknownError
}
enum Result {
case success(String)
case failure(Error)
}
lazy var networking: Networking = {
let networking = Networking(baseURL: APIClient.serverURL())
return networking
}()
func token(forUsername username: String, password: String, completion: #escaping (Result) -> Void) {
let parameters = ["username" : username, "password" : password]
networking.post("/api/login", parameters: parameters) { (result) in
switch result {
case .failure(let response):
return completion(.failure(response.error))
case .success(let response):
if var token = response.headers["Authorization"] as? String {
token = token.replacingOccurrences(of: "Bearer ", with: "")
return completion(.success(token))
}
return completion(.failure(ResponseError.unknownError))
}
}
}
}
Here for example I'm creating my own error return completion(.failure(ResponseError.unknownError)) in case the server responds with a successful status code (200) but somehow the Authorization header is missing from the response.
This works, the only problem is that now when I handle the error in the ViewController I don't know the exact reason why it fails. For example, from the Networking library I get an error code (400 or 401 etc) but this is lost because first it was an NSError. I could use an NSError but somehow this doesn't feel right. Could anyone point me in the right direction?
One of the solutions I thought of was to add an extra enum and then do something like this:
enum Result {
case success(String)
case networkFailure(FailureJSONResponse)
case failure(Error)
}
self.api.token(forUsername: "a", password: "b") { (result) in
switch (result) {
case .failure(let error):
print("something when wrong \(error)")
case .networkFailure(let response):
print("something when wrong \(error)")
case .success(let token):
print("token \(token)")
}
But I rather have just 1 success and 1 failure in the switch.

Every application usually has its own "Error", so in your case you can define
public enum AppError: Swift.Error, CustomStringConvertible {
case networkError(code: Int)
case anotherError(message: String?)
case underlying(Swift.Error)
public var description: String {
switch self {
case .networkError(let code):
return "Network error with code \(code)"
default://I'm not gonna cover all cases, but you should :)
return "Error"
}
}
}
then you can use your approach
enum Result {
case success(String)
case failure(AppError)
}

You can add second parameter to completion block:
func token(forUsername username: String, password: String, completion: #escaping (Result, error: ResponseError?) -> Void)

Related

How do I fix the prompt Open AI response?

I am using the OpenAI API to generate text completions for prompts, but I am encountering an issue where the API response returns an error
No value associated with key CodingKeys(stringValue: "object",
intValue: nil) ("object")
I have checked that the prompt I am sending is valid and I have tested with different prompts, but the issue persists. What could be causing this problem and how can I fix it? Any help or insights would be greatly appreciated.
Thank you.
class NetworkManager{
static let shared = NetworkManager()
#frozen enum Constants{
static let key = ""
}
private var client: OpenAISwift?
private init() {}
func setUp(){
self.client = OpenAISwift(authToken: Constants.key)
}
func getResponse(input: String, completion: #escaping (Result<String, Error>) -> Void) {
let prompt = """
translate from English to Nigerian Pidgin:
\(input)
Output:
"""
client?.sendCompletion(with: prompt, model: .gpt3(.davinci), maxTokens: 60) { result in
switch result {
case .success(let response):
let output = response.choices.first?.text ?? ""
print(response)
completion(.success(output))
case .failure(let error):
completion(.failure(error))
print(error.localizedDescription)
}
}
}
networkManager.getResponse(input: inputText) { result in
switch result {
case .success(let outputText):
DispatchQueue.main.async {
self.secondTextField.text = outputText
}
case .failure(let error):
print("Error translating text: \(error)")
}
}

how can i cast server error message to AFError

i am unable to find some way to cast custom message to AFError
class APIClient {
#discardableResult
static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:#escaping (Result<T, AFError>)->Void) -> DataRequest {
return AF.request(route).validate(statusCode: 200..<300).responseDecodable(decoder: decoder) { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let error):
let err = MYError(description: "here goes some custom error message")
let customErr = err .asAFError
completion(.failure(customErr!))
}
}
}
and my custom error message struct
struct MYError : Error{
let description : String
var localizedDescription: String {
return NSLocalizedString(description, comment: "")
}
Do you know what AFError you want to cast your error to? That's the first step to your answer because the AFError is a type of error class not one specific error. Here's my approach.
First I would change the error to an enum Errors defined as an Error just like the Struct you had. Then add your custom error as a case. Define your description and specify there what you would like the error message to be for each case (specific error).
Second, define what your Error should correspond to in an AFError for each case. This way you can specify and pass the data that will be required for some of the AFErrors like the example of SomeOtherError.
enum Errors: Error {
var localizedDescription: String {
var str: String = ""
switch self {
case .MyError: str = "Some custom error message"
case .SomeOtherError: str = "This would be another error and message"
}
return NSLocalizedString(str, comment: "")
}
var asAFError: AFError? {
switch self {
case .MyError: return .explicitlyCancelled
case .SomeOtherError(let data): return .sessionDeinitialized(data)
default: return nil
}
}
case MyError
case SomeOtherError(Data)
}
Lastly use a guard statement incase you throw and error that cannot be defined as an AFError, then pass that error to your completion like normal.
class APIClient {
#discardableResult
static func performRequest<T:Decodable>(route: APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:#escaping (Result<T, AFError>)->Void) -> DataRequest {
return AF.request(route).validate(statusCode: 200..<300).responseDecodable(decoder: decoder) { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let error):
guard let err = Errors.MyError.asAFError
else { return error }
completion(.failure(err))
}
}
}
}

can't return observable of customError in network call

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
}
}
}

Completion handler for Alamofire network fetch

I am attempting to create a function which will return a list of custom objects, created from parsing JSON. I am using AlamoFire to download the content. I have written this function which, on success, creates an array of locations to be returned. However, the returns are always nil. My code is below:
func fetchLocations() -> [Location]? {
var locations : [Location]?
Alamofire.request(.GET, myURL)
.responseJSON { response in
switch response.result {
case .Success(let data):
locations = createMapLocations(data)
case .Failure(let error):
print("Request failed with error: \(error)")
}
}
return locations
}
I am pretty positive the issue is that the functioning is returning before the network request is complete. I am new to Swift, and unsure how to handle this. Any help would be appreciated!
You can read more about closures/ completion handlers https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html or google.
func fetchLocations(completionHandler: (locations: [Location]?, error: NSError) -> ()) -> () {
var locations : [Location]?
Alamofire.request(.GET, myURL)
.responseJSON { response in
switch response.result {
case .Success(let data):
locations = createMapLocations(data)
completionHandler(locations, error: nil)
case .Failure(let error):
print("Request failed with error: \(error)")
completionHandler(locations: nil, error: error)
}
}
}
Usage
fetchLocations(){
data in
if(data.locations != nil){
//do something witht he data
}else{
//Handle error here
print(data.error)
}
}

How to use standard result type from Alamofile with no-type Success?

I'm trying to reuse Alamofire's Result type for own API callbacks.
Here is a shortened version of result type I'm using:
public enum Result<Value> {
case Success(Value)
case Failure(NSData?, ErrorType)
}
So for my API calls I'm using it in completion blocks:
func getUserContent(userId: String, completion: (result: Result<User>) -> Void) {
Alamofire.request(UserRouter.GetUser(userId))
.validate()
.responseJSON { (request, response, result) -> Void in
switch result {
case .Failure(_, let error):
completion(result: .Failure(nil, error))
case .Success(let value):
if let responseDict = value as? [String: AnyObject] {
do {
// synchronous call which parses response and
// creates User struct instance or throws exception
let user = try self.processUserResponse(responseDict)
completion(result: .Success(user))
} catch(let error) {
completion(result: .Failure(nil, error))
}
} else {
completion(result: .Failure(nil, MyAPIError.WrongResponseFormat))
}
}
}
}
I think its perfectly fits here but there is one issue. I have some calls with completion blocks which supposed to return either .Success with no value or .Failure.
E.g. deleteUser method should look something like:
func deleteUser(userId: String, completion: (result: Result<nil>) -> Void) {
// ... do some stuff here
}
so when I call it later I can do:
deleteUser(userId) { (result) -> Void in
switch result {
case .Success:
print("success")
case .Failure(nil, let error):
print("Error: \(error)")
}
}
But I can't create "empty" .Success. Result<nil> of course gives me a compile error. But I don't have any type to pass to some of .Success cases. Does anyone has a better solution that defining another Result Type with no type on .Success?
#ogreswamp is right! You can omit the type requirement with Void. Type Void is simply an empty tuple, in effect a tuple with zero elements, which can be written as (). Here is an example:
enum Result<T, E: ErrorType> {
case Success(T)
case Failure(E)
init(value: T) {
self = .Success(value)
}
init(error: E) {
self = .Failure(error)
}
}
Use this like
enum AuthenticationError: ErrorType {
case MissingEmail
case InvalidPassword
}
func signUp(email email: String, password: String) -> Result<Void, AuthenticationError>
You can return the result like
// Success
return Result(value: ())
// Failure
return Result(error: .InvalidPassword)
And finally, check the result
switch result {
case .Success(_):
print("Request SignUp was sent")
case .Failure(let error):
switch error {
case .InvalidEmail:
print("Invalid email")
default:
break
}
}
You will need to define your own Result type. Also note that Alamofire 3.0 uses a much different Result type that may better suit your needs.

Resources