iosswifterror-handlingnsurlsessiondatatask

URLSession.shared.dataTask correct way to receive data


I am a little confused in trying to find the correct sequence in checking received (data, response, error) from dataTask and doing some special error handling.

Usually we have URLSession looking like this:

class HTTPRequest {
    static func request(urlStr: String, parameters: [String: String], completion: @escaping (_ data: Data?,_ response: URLResponse?, _ error: Error?) -> ()) {
        var url = OpenExchange.base_URL + urlStr
        url += getParameters(param: parameters)
        let request = URLRequest(url: URL(string: url)!)
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if error != nil {
                print("URLSession Error: \(String(describing: error?.localizedDescription))")
                completion(nil,nil,error)
            } else {
                completion(data,response,nil)
            }
        }
        task.resume()
    }
    
    static func getParameters(param: [String: String]) -> String {
        var data = [String]()
        for (key,value) in param {
            data.append(key + "=\(value)")
        }
        return data.map { String($0) }.joined(separator: "&")
    }
    
}

I have another function that has HTTPRequest inside of it, to wrap everything to and object type I'm working with:

 static func networkOperation(urlStr: String, parameters: [String: String], completion: @escaping (ReturnedData) -> () ) {
        var recieved = ReturnedData()
        HTTPRequest.request(urlStr: urlStr, parameters: parameters) { (data, resp, err) in
            if let data = data, let response = resp {
                
// TODO: try JSONDecoder() if data is API Error Struct; Moderate this section depending on results of decoding;
                
                recieved.data = data
                recieved.response = response 
                recieved.result = .Success
                completion(recieved)
                return
            } else if err == nil {
                recieved.result = .ErrorUnknown
                completion(recieved)
                return
            }
            recieved.error = err as NSError?
            completion(recieved)
        }
       }

public struct ReturnedData {
    public var data: Data?
    public var response: URLResponse?
    public var error: Error?
    public var result: RequestResult = .ErrorHTTP
}

public enum RequestResult: String {
    case Success
    case ErrorAPI
    case ErrorHTTP
    case ErrorUnknown
}

Using the code above I can easily create different networkOperation calls for doing different API methods and handle different Data models that are returned. What I am trying to implement is API Error check. Since my API has some error description for example when you get your APP_ID wrong or current APP_ID has no permission to get information etc.. So if any of these occur the data will look like this:

  {
  "error": true,
  "status": 401,
  "message": "invalid_app_id",
  "description": "Invalid App ID provided - please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org."
  }

I think its not alright trying to decode every received data with Error struct in networkOperations "//TODO" mark, maybe there is some good way to implement this?


Solution

  • You should have your API errors return error objects.

    E.g. You could do:

    enum NetworkRequestError: Error {
        case api(_ status: Int, _ code: ApiResultCode, _ description: String)
    }
    

    Where you code your responses into an enum called ApiResultCode like so:

    enum ApiResultCode {
        case invalidAppId
        case recordNotFound   // just an example
        ...
        case unknown(String)
    }
    
    extension ApiResultCode {
        static func code(for string: String) -> ApiResultCode {
            switch string {
            case "invalid_app_id":   return .invalidAppId
            case "record_not_found": return .recordNotFound
            ...
            default:                 return .unknown(string)
            }
        }
    }
    

    This enum lets you check message codes without littering your code with string literals.

    And if you parse an API error, you could return that. E.g.

    if responseObject.error {
        let error = NetworkRequestError.api(responseObject.status, ApiResultCode.code(for: responseObject.message), responseObject.description)
        ... now pass this `error`, just like any other `Error` object
    }
    

    If you’re open to a broader redesign, I’d personally suggest

    So, first, let’s expand that RequestResult to include the error on failures and the payload on success:

    public enum Result {
        case success(Data)
        case failure(Error)
    }
    

    Actually, modern convention is to make this generic, where the above becomes a Result<Data, Error> using the following:

    public enum Result<T, U> {
        case success(T)
        case failure(U)
    }
    

    (Swift 5 actually includes this generic.)

    And I’d then expand ResultError to handle both API errors as well as any unknown errors:

    enum NetworkRequestError: Error {
        case api(_ status: Int, _ code: ApiResultCode, _ description: String)
        case unknown(Data?, URLResponse?)
    }
    

    So, having done this, you can change request to pass back a Result<Data, Error>:

    static func request(urlString: String, parameters: [String: String], completion: @escaping (Result<Data, Error>) -> ()) {
        let request = URLRequest(url: URL(string: urlString)!)
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            guard let responseData = data, error == nil else {
                completion(.failure(error ?? NetworkRequestError.unknown(data, response)))
                return
            }
    
            completion(.success(responseData))
        }
        task.resume()
    }
    

    And the caller would then do:

    request(...) { result in
        switch result {
        case .failure(let error):
            // do something with `error`
    
        case .success(let data):
            // do something with `data`
        }
    }
    

    The beauty of this Result generic is that it becomes a consistent pattern you can use throughout your code. For example, let’s assume you have some method that is going to parse a Foo object out of the Data that request returned:

    func retrieveFoo(completion: @escaping (Result<Foo, Error>) -> Void) {
        request(...) { result in
            switch result {
            case .failure(let error):
                completion(.failure(error))
    
            case .success(let data):
                do {
                    let responseObject = try JSONDecoder().decode(ResponseObject.self, from: data)
                    if responseObject.error {
                        completion(.failure(NetworkRequestError.api(responseObject.status, ApiResultCode.code(for: responseObject.message), responseObject.description)))
                        return
                    }
    
                    let foo = responseObject.foo
                    completion(.success(foo))
                } catch {
                    completion(.failure(error))
                }
            }
        }
    }
    

    Or, if you wanted to test for a particular API error, e.g. .recordNotFound:

    retrieveFoo { result in
        switch result {
        case .failure(NetworkRequestError.api(_, .recordNotFound, _)):
            // handle specific “record not found” error here
    
        case .failure(let error):
            // handle all other errors here
    
        case .success(let foo):
            // do something with `foo`
        }
    }