DevinM
DevinM

Reputation: 1312

Alamofire - How to get API error from AFError

In my quest to implement Alamofire 5 correctly and handle custom error model responses, I have yet to find an accepted answer that has an example.

To be as thorough as possible, here is my apiclient

class APIClient {
    
    static let sessionManager: Session = {
        let configuration = URLSessionConfiguration.af.default
        
        configuration.timeoutIntervalForRequest = 30
        configuration.waitsForConnectivity = true
        
        return Session(configuration: configuration, eventMonitors: [APILogger()])
    }()
    
    @discardableResult
    private static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:@escaping (Result<T, AFError>)->Void) -> DataRequest {
            return sessionManager.request(route)
//                .validate(statusCode: 200..<300) // This will kill the server side error response...
                .responseDecodable (decoder: decoder){ (response: DataResponse<T, AFError>) in
                    completion(response.result)
                }
        }
    
    static func login(username: String, password: String, completion:@escaping (Result<User, AFError>)->Void) {
        performRequest(route: APIRouter.login(username: username, password: password), completion: completion)
    }
}

I am using it like this

APIClient.login(username: "", password: "") { result in
    debugPrint(result)
    switch result {
    case .success(let user):
        debugPrint("__________SUCCESS__________")
    case .failure(let error):
        debugPrint("__________FAILURE__________")
        debugPrint(error.localizedDescription)
    }
}

I have noticed that if I use .validate() that the calling function will receive a failure however the response data is missing. Looking around it was noted here and here to cast underlyingError but thats nil.

The server responds with a parsable error model that I need at the calling function level. It would be far more pleasant to deserialize the JSON at the apiclient level and return it back to the calling function as a failure.

{
    "errorObject": {
        "summary": "",
        "details": [{
            ...
        }]
    }
}

UPDATE

Thanks to @GIJoeCodes comment I implemented this similar solution using the router.

class APIClient {
    
    static let sessionManager: Session = {
        let configuration = URLSessionConfiguration.af.default
        
        configuration.timeoutIntervalForRequest = 30
        configuration.waitsForConnectivity = true
        
        return Session(configuration: configuration, eventMonitors: [APILogger()])
    }()
    
    @discardableResult
    private static func performRequest<T:Decodable>(route:APIRouter, decoder: JSONDecoder = JSONDecoder(), completion:@escaping (_ response: T?, _ error: Error?)->Void) {
        
        sessionManager.request(route)
            .validate(statusCode: 200..<300) // This will kill the server side error response...
            .validate(contentType: ["application/json"])
            .responseJSON { response in
                
                guard let data = response.data else { return }
                do {
                    switch response.result {
                    case .success:
                        let object = try decoder.decode(T.self, from: data)
                        completion(object, nil)
                        
                    case .failure:
                        let error = try decoder.decode(ErrorWrapper.self, from: data)
                        completion(nil, error.error)
                        
                    }
                } catch {
                    debugPrint(error)
                }
            }
        }
    
    // MARK: - Authentication
    static func login(username: String, password: String, completion:@escaping (_ response: User?, _ error: Error?)->Void) {
        performRequest(route: APIRouter.login(username: username, password: password), completion: completion)
    }
}

Called like this

APIClient.login(username: "", password: "") { (user, error) in
    if let error = error {
        debugPrint("__________FAILURE__________")
        debugPrint(error)
        return
    }
    
    if let user = user {
        debugPrint("__________SUCCESS__________")
        debugPrint(user)
    }
}

Upvotes: 3

Views: 2481

Answers (2)

Jon Shier
Jon Shier

Reputation: 12770

First, there's no need to use responseJSON if you already have a Decodable model. You're doing unnecessary work by decoding the response data multiple times. Use responseDecodable and provide your Decodable type, in this case your generic T. responseDecodable(of: T).

Second, wrapping your expected Decodable types in an enum is a typical approach to solving this problem. For instance:

enum APIResponse<T: Decodable> {
  case success(T)
  case failure(APIError)
}

Then implement APIResponse's Decodable to try to parse either the successful type or APIError (there are a lot of examples of this). You can then parse your response using responseDecodable(of: APIResponse<T>.self).

Upvotes: 2

GIJoeCodes
GIJoeCodes

Reputation: 1850

This is how I get the errors and customize my error messages. In the validation, I get the errors outside of the 200..<300 response:

    AF.request(
        url,
        method: .post,
        parameters: json,
        encoder: JSONParameterEncoder.prettyPrinted,
        headers: headers
    ).validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .responseJSON { response in

        switch response.result {
        case .success(let result):
            let json = JSON(result)
            
            onSuccess()
            
        case .failure(let error):
            
            guard let data = response.data else { return }

            do {
                let json = try JSON(data: data)
                
                let message = json["message"]
                onError(message.rawValue as! String)

            } catch {
                print(error)
            }
            
            onError(error.localizedDescription)
        }
        
        debugPrint(response)
    }

Upvotes: 2

Related Questions