Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - Results and Validation Error Handling #627

Merged
merged 2 commits into from Jul 31, 2015

Conversation

Projects
None yet
10 participants
@cnoon
Copy link
Member

commented Jul 30, 2015

This PR refactors the response serializers to leverage Result types and also creates customized validation errors to resolve Issue #447.

Result Types

As I'm sure most people are aware, the double optional tuple scenario is Swift is a real problem resulting in us always having to check three cases even though there are really only two. Result types guarantee that you only ever have to account for the two situations.

Previous Style

The major area of Alamofire that previously suffered from this issue were the response serializers. Here is an example of the previous pattern:

Alamofire.request(.GET, URLString, parameters: ["foo": "bar"])
    .responseJSON { request, response, json, error in
        if let error = error {
            print("error occurred: \(error)")
        } else if let json = json {
            print("json response: \(json)")
        } else {
            print("something went really wrong b/c I didn't get json or an error")
        }
    }

New Style

After several revisions, the new way of extracting response JSON will leverage a Result type eliminating the third case:

Alamofire.request(.GET, URLString, parameters: ["foo": "bar"])
    .responseJSON { request, response, result in
        switch result {
        case .Success(let JSON):
            print("Success with JSON: \(JSON)")
        case .Failure(let data, let error):
            print("Request failed with error: \(error)")

            if let data = data {
                print("Response data: \(NSString(data: data, encoding: NSUTF8StringEncoding)!)")
            }
        }
    }

The result success value is guaranteed to be returned as an actual value, not an optional. Additionally, in the case of a failure, the error is guaranteed and the original data returned from the server is returned as an NSData optional. All default response serializers have been refactored to ensure these cases are always upheld. The required reworking the error system in Alamofire.

There are also convenience computed properties on the Result type to make it really easy to check for things like a success or extract the success value out:

Alamofire.request(.GET, URLString, parameters: ["foo": "bar"])
    .responseJSON { request, response, result in
        if let JSON = result.value {
            print("JSON Response: \(JSON)")
        } else {
            print("Error: \(result.error)")
        }
    }

Bonus Points

The coolest part about this set of changes is that the old response method was kept in tact. There are certainly cases where you may not want to use a response serializer, but you just want to hook into the response to know that the task delegate queue processing is complete. Therefore, the original response method still exists, but no longer includes a responseSerializer parameter. It simply calls the completionHandler immediately.

public func response(
    queue: dispatch_queue_t? = nil,
    completionHandler: (NSURLRequest?, NSHTTPURLResponse?, NSData?, NSError?) -> Void)
    -> Self
{
    delegate.queue.addOperationWithBlock {
        dispatch_async(queue ?? dispatch_get_main_queue()) {
            completionHandler(self.request, self.response, self.delegate.data, self.delegate.error)
        }
    }

    return self
}

This allows you to simply tap into the completion of the task without running any extra processing as though you implemented the session delegate completion method directly.

Validation Errors

Previously, if response validation failed, you simply received an NSError with a com.alamofire.error domain with a -1 error code. This made it quite difficult to figure out whether content type or status code validation actually failed.

With the new structure, content type and status code validation have separate errors making it really easy to tell what actually went wrong.

public struct Error {
    public static let Domain = "com.alamofire.error"

    public enum Code: Int {
        case InputStreamReadFailed           = -6000
        case OutputStreamWriteFailed         = -6001
        case ContentTypeValidationFailed     = -6002
        case StatusCodeValidationFailed      = -6003
        case DataSerializationFailed         = -6004
        case StringSerializationFailed       = -6005
        case JSONSerializationFailed         = -6006
        case PropertyListSerializationFailed = -6007
    }
}

Summary

These changes should simplify the process of handling success and failure cases for responses. I would love feedback from the community here to see if there are any edge cases that I haven't thought of. Please keep in mind that we have MANY different edge cases to consider here before being to quick to recommend other solutions. As I mentioned earlier, this went through several revisions before being pushed up.

  • There are data, download and upload tasks that the response serializers ALL need to support.
    • Downloads behave MUCH differently and don't return any data.
  • You need to be able to access the original server data in the case that an error occurred.
  • You want to avoid optionals at all costs in the case of a success or failure to avoid having to optional bind inside a switch.

Thanks in advance for taking a look!

@cnoon

This comment has been minimized.

Copy link
Member Author

commented Jul 30, 2015

@saniul

This comment has been minimized.

Copy link

commented Jul 30, 2015

👍 This is exactly the approach we took in the layer that wraps Alamofire calls. Would love to get rid of handling that “third case” in our code!

@kostiakoval

This comment has been minimized.

Copy link

commented Jul 30, 2015

Same here, now I can get rid of my own Result type. I love that! 👏 👍 💯

@cnoon

This comment has been minimized.

Copy link
Member Author

commented Jul 31, 2015

Okay, I'm going to merge this in. Thanks for the feedback @saniul and @kostiakoval!

cnoon added a commit that referenced this pull request Jul 31, 2015

@cnoon cnoon merged commit fedbc04 into swift-2.0 Jul 31, 2015

1 check failed

continuous-integration/travis-ci/push The Travis CI build failed
Details

@cnoon cnoon deleted the feature/results_and_validation_error_handling branch Jul 31, 2015

@kylef

This comment has been minimized.

Copy link
Contributor

commented Aug 3, 2015

@cnoon This pull request doesn't update the examples/documentation in the README. Would you be able to do that in another PR? (re #641).

@cnoon

This comment has been minimized.

Copy link
Member Author

commented Aug 3, 2015

Thanks for the heads up @kylef. I usually always do that as part of the original PR and this time I just forgot. It's definitely the next thing I'm going to work on. Should have it done today or tomorrow.

@kcharwood

This comment has been minimized.

Copy link
Contributor

commented Aug 3, 2015

👍

@cnoon

This comment has been minimized.

Copy link
Member Author

commented Aug 6, 2015

Hey @kylef and @kcharwood, README updates for Result types are up in PR #648! 👍🏼

@cristeahub cristeahub referenced this pull request Aug 14, 2015

Closed

Result<AnyObject> #688

@pranavss11

This comment has been minimized.

Copy link

commented Aug 17, 2015

@cnoon
I've been calling .validate() before calling .responseObject (As documented in the README). Let's assume for a second that the server returns a 401 status code (which would mean that in the responseObject, the failureReason would be whatever it is in the validate method).

What if the server was sending a json back that describes what the actual error was? How would I get that to pass along with validate?

Thanks!

@blured2000

This comment has been minimized.

Copy link

commented Aug 18, 2015

@pranavss11

case .Failure(let data, let error):

The data is the actual error response from the server.

Example:
If the json error response back from the server looks like this:

{"message":"Error description"}

You can do something like this:

let task = Alamofire.request(urlRequest)
            .validate()
            .responseJSON { (request, response, result) in

                switch result {

                case .Success(let json):

                    print("JSON Response: \(JSON)")

                case .Failure(let data, let error):

                    let errorData = NSString(data: data!, encoding:NSUTF8StringEncoding)

                    print("error message: \(errorData!)")
                }
        }
@cesar-oyarzun-m

This comment has been minimized.

Copy link

commented Sep 10, 2015

@blured2000 Hey I'm ussing responseObject for my calls. I'm getting the statuscode but I can get the JSON error how can I do this? this is my code

    let responseSerializer = GenericResponseSerializer<T> { request, response, data in

        let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
        let (JSON: AnyObject?, serializationError) = JSONResponseSerializer.serializeResponse(request, response, data)

        let result = JSON as? NSDictionary
        let codeKey: AnyObject? = result?["code"]
        if codeKey == nil{
            if let response = response, JSON: AnyObject = JSON {
                return (T(response: response, representation: JSON), nil)
            } else {
                return (nil, serializationError)
            }

        }else{
            return (nil, serializationError)
        }
@cnoon

This comment has been minimized.

Copy link
Member Author

commented Sep 12, 2015

The JSON error is going to be in the .Failure case of a result.

@cesar-oyarzun-m

This comment has been minimized.

Copy link

commented Sep 17, 2015

@cnoon but when you do Mapping is the same?, I'm mapping the result to an Object like this

public func getDevices(limit:String, skip:String, sort:String) -> Promise<Array<Device>>{
    return Promise { fulfill, reject in
            let request = Alamofire.request(.GET, hostUrl + "/api/devices?" + "limit=" + limit + "&skip=" + skip + "&sort=" + sort )
               request.responseCollection { (request, response, devices: [Device]?, error) in

                var statusCode = response?.statusCode
                    if(error != nil) {
                        return reject(error!)
                    }
                    if(statusCode < 200 || statusCode > 299) {
                        return reject(NSError(domain: hostUrl + "/api/devices", code: statusCode!, userInfo: [:]))
                    }
                    fulfill(devices!)
            }
}
}

I'm using alamofire 1.3.1

@mamouneyya

This comment has been minimized.

Copy link

commented Apr 2, 2016

As I see, data object eventually got removed from .Failure case in latest version. As of this change, how can we extract an error message from response body when request fails?

@clooth

This comment has been minimized.

Copy link

commented Apr 12, 2016

I'm wondering this too. What's the go-to way to get the body of the response when things fail?

Edit: You can find an optional NSData in response.data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.