tristanhimmelman/AlamofireObjectMapper

How to handle error responses

Opened this issue ยท 17 comments

Hi,

I use your .responseArray() method to query for json arrays. This works without problems if the query is successful. But if the query fails I get the following response:

{
  "status": "api key invalid"
}

So now my response object (array) is invalid. Is there any way to parse the status message with an different response class, if the first one fails?

Or who should i handle this?

Why you don't use http header status code?

@AFcgi I use the http header status code. But I would be able to show the user the error message (status hash)

This is an interesting use case that I have not considered.

If you control the API I would suggest have all requests formed as follows:

{
   "status": true,
   "data": {...}
}

{
   "status": false,
   "error" : "error message goes here"
}

If you don't control the API, this a bit trickier to handle... perhaps we can include the response string in the returned error?

I've just faced the same issue and my solution is to implement a generic class that wraps the object and the error:

class FailableObject<T: Mappable, E: Mappable> : Mappable {
    var object: T?
    var apiError: E?

    required init?(_ map: Map) {}

    func mapping(map: Map) {
        apiError = E(map)
        if apiError != nil {
            apiError!.mapping(map)
        } else {
            object = T(map)
            if object != nil {
                object!.mapping(map)
            }
        }
    }
}

Entities validation and mapping:

class User: Mappable {

    private struct Keys {
        static let userId = "UserId"
        static let tokenString = "TokenString"
        static let expirationDate = "ExpirationDate"
        static let email = "Email"
    }

    required init?(_ map: Map) {
        let requiredKeys = [Keys.userId, Keys.tokenString]

        let keys = Array(map.JSONDictionary.keys)
        var containsAllRequiredKeys = true

        for key in requiredKeys {
            if !keys.contains(key) {
                containsAllRequiredKeys = false
                break
            }
        }

        if !containsAllRequiredKeys {
            return nil
        }
    }

    func mapping(map: Map) { ... }
}

class APIError: Mappable {
    // mapping logic goes here, similar to the 'User' code
}

And after that you can use it like that to fetch the User entity for example:

typealias FailableUser = FailableObject<User, APIError>
Alamofire
    .request(.........)
    .responseObject { (response: Response<FailableUser, NSError>) in 
        if let failableUser = response.result.value {
            if let user = failableUser.object {
                // handle User object
            }
            else if let apiError = failableUser.apiError {
                // handle API error
            }
            else {
                // ...
            }
        }
        else {
            // ...
        }
    }

This code is for the object mapping but I beleive it can be changed for arrays as well.
NB: I have written the answer without Xcode so there may be some mistakes, sorry for that ;) But the idea should be clear I think.

Update: I've updated the code and now it should work without errors (checked it in Xcode). Hope that helps.

@svyatoslav-zubrin first of all thank you for the awesome trick, i tried it but i am getting errors. when i initialize the object = T() the error says cannot invoke empty initializers of type 'T' and also object.mapping is not working as the error say T does not have mapping as a member

@kunalcodes try the updated code, please. Note, that now I placed validation logic inside the initializer of the entity class.

@svyatoslav-zubrin thanks for your input above code is working like a charm. I tried to apply the same logic with little bit modification for fetching array of objects but it is not working, if you have implemented or have any idea can you enlighten me.

Hi,
This solution works fine for me.

Create a class to handle API errors :

public class APIError: Mappable {

    var message: String?

    required public init?(_ map: Map) {

    }

    public func mapping(map: Map) {
        message <- map["message"]
    }

    func getMessage() -> String? {
        return self.message
    }
}

Now you can create your own class which inherits APIError and custom the getMessage func to return some message in case of failure.

Override ObjectMapperSerializer

public static func ObjectMapperSerializer<T: Mappable, E: APIError>(keyPath: String?, mapToObject object: T? = nil, typeOfError errorType: E.Type) -> ResponseSerializer<T, NSError> {
        return ResponseSerializer { request, response, data, error in
            // ...
            let JSONToMap: AnyObject?
            if let keyPath = keyPath where keyPath.isEmpty == false {
                JSONToMap = result.value?.valueForKeyPath(keyPath)
            } else {
                JSONToMap = result.value
            }

            // API error logic
            switch response!.statusCode {
            case 200..<400:
                break
            default:
                if let errorParsed = Mapper<E>().map(JSONToMap) {
                    let failureReason = errorParsed.getMessage()
                    let error = Error.errorWithCode(.StatusCodeValidationFailed, failureReason: failureReason ?? "ObjectMapper failed to serialize response.")
                    return .Failure(error)
                }
            }
            // ...
        }
    }

Then override responseObject

public func responseObject<T: Mappable, E: APIError>(queue queue: dispatch_queue_t? = nil, keyPath: String? = nil, mapToObject object: T? = nil, typeOfError errorType: E.Type, completionHandler: Response<T, NSError> -> Void) -> Self {
        return response(queue: queue, responseSerializer: Request.ObjectMapperSerializer(keyPath, mapToObject: object, typeOfError: errorType), completionHandler: completionHandler)
    }

Now you can make your request like this

Alamofire.request(...)
            .responseObject(typeOfError: YourCustomErrorClass.self) { (response: Response<YouCustomClass, NSError>) in
                if let object = response.result.value {
                    // Handle retrieved object
                } else {
                    if let message = response.result.error?.localizedFailureReason as String! {
                        // Handle API error message
                    }
                }
        }

The class YourCustomErrorClass must inherit from APIError class

You can apply the same logic for arrays.

@svyatoslav-zubrin is it possible to apply this solution for Arrays? I couldnt manage it to work.

@miko91 I was not able to get the solution to work. Do you by chance have a working example somewhere on GitHub? Much appreciated.

@kunalcodes, @KralMurat It is not so elegant and obvious for arrays as for objects but the idea is the same: wrap the array of objects and the error. But here we have a bit more work to do.

Wrap the array of objects:

struct FailableArrayOfObjects <T: Mappable, E: Mappable> : Mappable {

    var array: [T]?
    var apiError: E?

    init?(_ map: Map) {}

    mutating func mapping(map: Map) {
        apiError = E(map)
        if apiError != nil {
            apiError!.mapping(map)
        } else {

            array = [T]()
            if array != nil {
                array = Mapper<T>().mapArray(map.failableJSONArray())
            }
        }
    }
}

We'll need a separate response serializer and mapper for that.

extension Request {
    public func responseFailableArrayOfObjects<T: Mappable>(queue queue: dispatch_queue_t? = nil, keyPath: String? = nil, completionHandler: Response<T, NSError> -> Void) -> Self {
        return response(queue: queue, responseSerializer: Request.FailableObjectMapperSerializer(keyPath), completionHandler: completionHandler)
    }

    public static func FailableObjectMapperSerializer<T: Mappable>(keyPath: String?) -> ResponseSerializer<T, NSError> {
        return ResponseSerializer { request, response, data, error in
            guard error == nil else {
                return .Failure(error!)
            }

            guard let _ = data else {
                let failureReason = "Data could not be serialized. Input data was nil."
                let errorInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
                let error = NSError(domain: Alamofire.Error.Domain, code: Alamofire.Error.Code.DataSerializationFailed.rawValue, userInfo: errorInfo)
                return .Failure(error)
            }

            let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
            let result = JSONResponseSerializer.serializeResponse(request, response, data, error)

            var JSONToMap: AnyObject?
            if let keyPath = keyPath where keyPath.isEmpty == false {
                JSONToMap = result.value?.valueForKeyPath(keyPath)
            }

            if JSONToMap == nil {
                JSONToMap = result.value
            }

            if let parsedObject = Mapper<T>().mapFailableArray(JSONToMap){
                return .Success(parsedObject)
            }

            let failureReason = "ObjectMapper failed to serialize response."
            let errorInfo = [NSLocalizedFailureReasonErrorKey: failureReason]
            let error = NSError(domain: Alamofire.Error.Domain, code: Alamofire.Error.Code.DataSerializationFailed.rawValue, userInfo: errorInfo)
            return .Failure(error)
        }
    }
}

extension Mapper {
    public func mapFailableArray(JSON: AnyObject?) -> N? {
        if let JSON = JSON as? [[String : AnyObject]] {
            return map(JSON)
        } else if let JSON = JSON as? [String: AnyObject] {
            return map(JSON)
        }

        return nil
    }

    public func map(JSONArray: [[String: AnyObject]]) -> N? {
        let JSONDictionary = [Map.failableJSONArrayKey: JSONArray]
        let map = Map(mappingType: .FromJSON, JSONDictionary: JSONDictionary)

        if var object = N(map) {
            object.mapping(map)
            return object
        }
        return nil
    }
}

extension Map {
    static let failableJSONArrayKey = "FailableAlamofireObjectMapper.Map.JSONDictionary.array"

    func failableJSONArray() -> [[String: AnyObject]]? {
        return JSONDictionary[Map.failableJSONArrayKey] as? [[String: AnyObject]]
    }
} 

And use all that stuff like:

typealias FailableArrayOfUsers = FailableArrayOfObjects<User, APIError>
    Alamofire
        .request(...)
        .responseFailableArrayOfObjects(queue: nil, keyPath: "Users") { (response: Response<FailableArrayOfUsers, NSError>) in
            switch response.result {
            case .Success(let failableUsers):
                if let users = failableUsers.array {
                    // 'users' should be of type "Array<User>" here
                } else if let apiError = failableUsers.apiError {
                    // handle API error here 
                } else {
                    // something goes wrong, handle that correctly
                }
            case .Failure(let error):
                // handle error here
            }
        }

One more thing: the proposed solution looks like workaround to me and I don't really like it but it worked in one of my projects. In particular I don't like that at the end you need to remember to use 'responseFailableArrayOfObjects' function, 'FailableArrayOfUsers' typealias and 'failableUsers.array' in one place of the final code - all that looks overabundant I think. If you have any idea how to improve that - let me know please.

@svyatoslav-zubrin can you write your solution for handle array with AlamofireObjectMapper for swift3? Thanks

@tristanhimmelman I've got the same issue when dealing with the Github API. The response.result.description is SUCCESS when I got the status code 403, and there is nothing I can do for getting the error message from the DataResponse. It would be a great improvement that adding a new attribute to response.result or something else for error information.

Hey there :)
Alamofire.Error.Code.DataSerializationFailed.rawValue has been removed from Alamofire. Till now I made a check like this:
if response.result.error.code == Alamofire.Error.Code.DataSerializationFailed.rawValue

How can this be handled now? I fixed it this way, but I think it's not a good solution:
if (response.reult.error! as NSError).code == 2

Any hints, how to check for a data serialization error? I found the dataSerializationFailed in an enum in the AlamofireObjectMapper.swift file, but can't access it.

Thanks!

I have written a class to handle an error response since I needed it for my own implementation: https://github.com/Shanlon/Alamofire-ObjectMapper/blob/master/Networking.swift

@luli-isa Were u able to find out the solution to handle array with AlamofireObjectMapper for swift3 or swift 4?

you can using response.result

AF.request(baseURL).responseObject{(response:AFDataResponse<{YourResponseModel}>) in
            switch response.result {
            case .success(let data):
                let mappedObject = response.value
                print(mappedObject)
            case .failure(let error):
                print("---> error \(error)")
            }
        }