AFNetworking/AFNetworking — pull request #157 by kcharwood (2011-12-20 21:32:43+00:00)
Title: AFNetworking + Reachability

One missing piece in AFNetworking when comparing to ASI is the ability to leverage Reachability when making a request. If the host is not reachable, the request should immediately fail instead of waiting for the timeout. This is especially important for POST requests, since Apple for some [unknown reason](https://devforums.apple.com/message/108292#108292) decided it would be in everyones best interest to set a minimum timeout value of 240 seconds with no way to change it. Waiting for a request to fail for 4 minutes is ridiculous.

With that in mind, I wanted to take a stab at adding in Reachability. My initial design is setup to be more of an extension to AFNetworking than a piece that lives inside of the main branch (Similar to AFHTTPRequestOperationLogger, AFOAuth2Client, etc). There are a couple of advantages to this approach, namely being AFNetworking can continue to be a light weight library that requires no other additions, while users can choose to extend the base functionality by bringing in other various extensions. In the case of this extension I have created, you would need to bring in Reachability and SystemConfiguration.framework in addition to AFReachableClient. This is actually probably a larger discussion on whats the best way to extend functionality of AFNetworking and provide that to the community. I would love to here @Mattt's thoughts on that.

Now, on to what I have actually implemented. I set out with a few goals here:

1. Leverage Apple's Reachability class to cancel operations when a network connection is lost
2. Create a drop-in subclass of AFHTTPClient
3. Don't touch or modify anything upstream in AFNetworking

**The Approach**
I have created a subclass of AFHTTPClient called AFReachableClient. This object is responsible for maintaining a Reachability object for a specific host URL, as well as canceling operations if a network connection is lost. In AFReachableClient, I do this in two places. In enqueueHTTPRequestOperation:, I check the reachability status of the connection, and if is not available, I fail this operation and do not enqueue it. This handles all new operations that are enqueued after the network connection is lost. For operations that are in the operationQueue, I use the reachabilityChanged callback to grab all the operations and properly cancel them. I have also included a test project to demonstrate the proper behavior.

**Things I need changed in AFNetworking**
This leads me to goal 3 above as I was ALMOST able to achieve this, but in the end had to make a few minor changes. I would really love feedback on these in particular just to make sure I have not broken something else. The biggest change is in the setCompletionBlock method for all the AFHTTPOperations. In the current AFNetworking implementation, if an operation was cancelled it will immediately return and not call the fail block or the success block. This obviously isn't ideal for what I am trying to achieve since the AFReachableClient can cancel the request, and the caller would need to get feedback that it did in fact fail with error. I went through each AFHTTPOperation, and modified the setCompletionBlock to return immediately if it has been cancelled AND its' error is nil. **@Mattt - any major problem adding the additional check to ensure a failure block gets called if a request is cancelled and its' error is not nil?** It seems to me that if there was an error, the caller should get the failureBlock called with that error. Existing behavior should continue to work the same since when people cancel the request without manually setting an error, it will return without calling the failure block or successBlock. Below is the new method:

```objective-c
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
self.completionBlock = ^ {
if ([self isCancelled] &&
self.error == nil) {
return;
}

if (self.error) {
if (failure) {
dispatch_async(dispatch_get_main_queue(), ^(void) {
failure(self, self.error);
});
}
} else {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
success(self, self.responseData);
});
}
}
};
}
```

In addition, I needed a way to manually set an error on an operation from outside the operation. For now, I have simply exposed HTTP error on AFHTTPOperation. This gets me the behavior I need, but I am guessing that error was private for a reason so it may not be the best long term solution. **Should we have a user error that can be manually set on the operation?**

**Example**
I have also included an example that hits Yahoo's weather API. Every time you press the button, 30 operations are queued up and executed one at a time. This gives you time to kill a network connection in the middle of the operationQueue, and see the resulting behavior.

**Feedback**
Would love feedback/suggestions on the best way to implement this. I would really like to see a place for developers to contribute AFNetworking extensions.