Sometimes a POST request with AFHTTPSessionManager executes the failure block with the following error: The network connection was lost. BUT the server side code executes successfully, sending a 200 OK response. How is that possible and what to do about it? What is a good strategy in general if a network connection is lost while the server executes a routine (successfully)? Could it be a problem with the AFNetworkReachabilityManager? I have it set up like this in the AppDelegate:
// Monitor network connection
[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
switch (status) {
case AFNetworkReachabilityStatusReachableViaWWAN:
case AFNetworkReachabilityStatusReachableViaWiFi:
DDLogInfo(#"AFNetworkReachabilityStatusReachableViaWiFi:");
// Resume callbacks for remote operations
[[[TSHTTPSessionManager sharedManager] operationQueue] setSuspended:NO];
// Notify app
[[NSNotificationCenter defaultCenter] postNotificationName:TSDidConnectToNetworkNotification
object:self userInfo:nil];
break;
case AFNetworkReachabilityStatusNotReachable:
DDLogInfo(#"AFNetworkReachabilityStatusNotReachable:");
// Suspend callbacks for remote operations
[[[TSHTTPSessionManager sharedManager] operationQueue] setSuspended:YES];
// Notify app
[[NSNotificationCenter defaultCenter] postNotificationName:TSDidDisconnectFromNetworkNotification
object:self userInfo:nil];
break;
default:
break;
}
}];
EDIT: I guess I did not make myself clear: The question is not about AFNetworkReachabilityManager (I just added the code to show how the app detects connectivity issues and forwards them as notifications such that other parts of the app get notified if they decide to do so). The questions is about a request (POST) which is transmitted successfully to the server which causes the server side code to execute. At the same time, and before the server is able to send its response, the connection is lost, which in turn causes the client side failure block to execute (after the network is reachable again but that does not really make a difference I guess). How do I know on the client side that the server succeeded nonetheless?
If a device loses connectivity, AFNetworking's underlying AFHTTPRequestOperation and NSURLSessionDataTask objects will fail immediately. All you're doing by suspending the operationQueue is preventing the completion blocks you've set from running, so you don't hear about the failure until the connection comes back online and the operationQueue is resumed.
In general, when dealing with connectivity issues, it's simplest merely to handle it like any other failure. Perhaps present an error message to the user letting them know their connection is offline and to try their request again when connectivity is restored. If you really, really need to you can build a convenience wrapper around the various AFNetworking calls that attempts to retry them on failure or loss of connectivity, but that seems like a lot of work for little benefit, given you have no idea when connectivity will resume. For all you know the user has switched away from your app and it was killed in the background. But your current approach is certainly not the right way.
Related
I get a requirement to be able to check if there is data with fail status and upload them via webservice.
The flow is gonna be:
User try to upload data but fail because of no internet connection
User stops using app, so the app goes background
Apps start to identify whether internet connection is available
If available, it directly calls web service to upload data
If not available, it will return to #3
Back to my last development on < iOS 7, previously background time was 10 minutes but becomes 3 minutes.
Background Fetch:
When this method is called, your app has up to 30 seconds of
wall-clock time to perform the download operation and call the
specified completion handler block. In practice, your app should call
the completion handler block as soon as possible after downloading the
needed data. If you do not call the completion handler in time, your
app is terminated.
Also that these are the only apps allowed to do background:
Apps that play audible content to the user while in the background, such as a music player app
Apps that record audio content while in the background.
Apps that keep users informed of their location at all times, such as a navigation app
Apps that support Voice over Internet Protocol (VoIP)
Apps that need to download and process new content regularly (Background Fetch)
Apps that receive regular updates from external accessories
Isn't there any way I can do background in iOS yet or at least I can get the network status in background?
Note: My app is for internal distribution only, no need to worry with rejection.
What you need to do is using NSURLSession instead of NSURLConnection (in case you are still using it)
Difference between NSURLSession and NSURLConnection
NSURLConnection: if we have an open connection with NSURLConnection and the system interrupt our App, when our App goes to background mode, everything we have received or sent were lost.
NSURLSession: solve this problem and also give us out of process downloads. It manage the connection process even when we don't have access. You will need to use application:handleEventsForBackgroundURLSession:completionHandler in your AppDelegate
So with the use of NSURLSession, you don't need to manage or to check
your internet connection because OS does it for you.
Extra: How to check internet connection in my iOS app
Disclaimer: I'm not sure that the following Framework can run in background mode
You can use framework Rechability to check if your device have internet connection.
https://github.com/tonymillion/Reachability
It is a drop-in replacement for Apple's Reachability class and it supports blocks to perform action when you receive the NSNotification of reachability changed.
You can check the README.md at his repo but it is very simple to use.
Using NSNotification
1
In your AppDelegate you have to call
[Reachability reachabilityWithHostname:#"www.google.com"];
Also, you will need to declare a property to store that value
#property (strong, nonatomic) Reachability *reach;
so now you will have to put the following code inside your application:didFinishLaunchingWithOptions:
_reach = [Reachability reachabilityWithHostname:#"www.google.com"];
2
In the Controller you need to check if reachability state, you must subscribe to that notification
[[NSNotificationCenter defaultCenter] addObserver:_sharedInstance selector:#selector(reachabilityDidChange:) name:#"kReachabilityChanged" object:nil];
3
Now you can implement the selector reachabilityDidChange: where you check if now are connected or not. Put it in the same Controller.
- (void)reachabilityDidChange:(NSNotification *)note
{
Reachability * reach = (Reachability *)[note object];
if (self.appDelegate.wasConnected != [reach isReachable]){
if ([reach isReachable]){
NSLog(#"Notification Says Reachable");
[[NSNotificationCenter defaultCenter] postNotificationName:#"ConnectionIsReachable" object:nil];
[self sendPendingOperations];
}else{
[[NSNotificationCenter defaultCenter] postNotificationName:#"ConnectionIsUnreachable" object:nil];
NSLog(#"Notification Says Unreachable");
}
self.appDelegate.wasConnected = [reach isReachable];
}
}
As you can see, we store the last connection state also in a AppDelegate property
`#property BOOL wasConnected;`
4
Now you have to subscribe to those new notifications (ConnectionIsReachable, ConnectionIsUnreachable) and perform the action you need to do.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(showNoConnectionAlert) name:#"ConnectionIsUnreachable" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(hideNoConnectionAlert) name:#"ConnectionIsReachable" object:nil];
Using blocks
I have not test it that feature but I copy to you the official documentation
// Allocate a reachability object
Reachability* reach = [Reachability reachabilityWithHostname:#"www.google.com"];
// Set the blocks
reach.reachableBlock = ^(Reachability*reach)
{
// keep in mind this is called on a background thread
// and if you are updating the UI it needs to happen
// on the main thread, like this:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"REACHABLE!");
});
};
reach.unreachableBlock = ^(Reachability*reach)
{
NSLog(#"UNREACHABLE!");
};
// Start the notifier, which will cause the reachability object to retain itself!
[reach startNotifier];
I would like to know how it is possible to continue a async NSURLConnection, which has been started in the foreground, when the app will be terminated.
Currently I am starting a NSURLConnection when the app goes in the background. This works fine as long as the user is slower than the connection, when he wants to terminate the app. But when the user is quicker than it, the connection can't be established.
Here is my code:
// AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application
{
AnObject *newObject = [[AnObject alloc] init];
[newObject InactiveApp];
}
// AnObject.m
- (void)InactiveApp
{
self.backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
// setting up the NSURLRequest
// [...]
dispatch_async(dispatch_get_main_queue(), ^
{
[NSURLConnection connectionWithRequest:request delegate:self];
});
}
// delegate functions, endBackgroundTask-closing, etc. is following
Unfortunately this is not working and I would like to know whether someone knows another way to fix it. There has to be a way which is similar like Snapchat or WhatsApp is doing it, because when you write an message and terminate the app right after pressing send, the message will be delivered.
The only way I could imagine is to do it with a background fetch but I think that is not the best solution, due to the fact that I just want to make one single connection when the App is send to the background.
I agree with Andy, that you should pursue NSURLSession and a background NSURLSessionConfiguration. See downloading content in the background section of the App Programming Guide for iOS: Background Execution.
By the way, the idea in your question will work fine (especially if you need support for iOS versions prior to 7.0, where NSURLSession and its background sessions are not available). Two observations regarding your code snippet:
The way you've written it, would appear that your AnObject would be prematurely deallocated when it falls out of scope and your app would therefore fail when it tried to call the delegate methods. Make sure to maintain a strong reference to AnObject.
Don't forget to call endBackgroundTask when the download is done. Likewise (and more subtly), the timeout handler should end the background task, too. See the Executing Finite Length Task section of the aforementioned App Programming Guide.
By the way, you mention requests continuing after the app is terminated. If a user manually terminates an app, that kills both background tasks contemplated in your question as well as background NSURLSession tasks. These are intended to gracefully handle continuing tasks if the app leaves foreground, not if the user manually terminates the app. The NSURLSession approach gracefully handles terminations due to memory pressure, but not manual termination.
I'm a bit lost on AFNetorking's Reachability and haven't found a lot of good information out there.
I have an app that logs into a web API. Each VC connects to the API in some way so each VC needs to have network access. I use a subclass of AFHTTPSessionManager to make network requests.
I am confused as to where to put the code - is it just the appDelegate or is it in each VC?
What is the use of this code?
[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
NSLog(#"Reachability: %#", AFStringFromNetworkReachabilityStatus(status));
}];
And where do you use these methods and how do they relate to the code above?
-startMonitoring
-stopMonitoring
The -startMonitoring method of AFNetworking is used to make your AFNetworkReachabilityManager start monitoring the network connection. If you do not call -startMonitoring, your setReachabilityStatusChangeBlock will never be called (regardless of where you put it) since AFNetworking isn't monitoring your connection.
As you probably assumed, -stopMonitoring does the exact opposite of -startMonitoring - it stops the manager from checking network connectivity. I normally don't use this method in my apps, but if your app doesn't need a network connection for a certain part of it, feel free to call it (just make sure you start monitoring the network again when you need to).
The setReachabilityStatusChangeBlock is called whenever the network status changes. You can put this wherever you want to make changes if/when the network changes. An example of this would be putting it in your app delegate and sending out a NSNotification when the network status changes so that any view controller observing the notification can react appropriately.
You can also manually check to see if the network is online (as long as -startMonitoring was called and AFNetworking is monitoring your network) using something like this:
if ([[AFNetworkReachabilityManager sharedManager] isReachable]) {
NSLog(#"Online");
}
You can read more on AFNetworking's official documentation on GitHub.
I'm using NSURLConnection to send an HTTP request and running it with
[[NSURLConnection alloc] initWithRequest:self.request delegate:self];
where "self.request" is a configured NSMutableURLRequest object. Upon a network failure and callback to
-(void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
I'd like to cancel the request and write the payload to file to be uploaded later. However if I reconnect too quickly the payload ends up getting sent still (and I later send the same payload from the file). In the failure callback, I've tried to use
[connection cancel];
But the http request still goes through upon reconnecting within a few seconds. Is this due to some retry mechanic that I can disable?
From Apple's docs on didFailWithError:
// Once the delegate receives this message, it will receive no further messages for connection.
So I'd say you have a bug somewhere else in the code. Once that delegate message is called, the connection is done and it will never be restarted unless you do so.
From the question it seems like you are trying to cancel the connection in the failure delegate method, is that correct? That should not be necessary since the connection is already dead. I'm also a bit confused when you say you "retry to quickly". I don't see any "retry" code so it would help if you could post that too.
Can you post more code to help diagnose the issue?
I've got NSURLConnection with timeout = 30s which is uploading an image on server.
If connection is horrible and call delegate method didFailWithError: then i need to cancel current connection.
But if i just call the [myConnection cancel] connection will still alive but will not call delegates methods (apple docs say it - NSURLConnection cancel method). And i want to abort connection but not only remove delegate methods. How i can do what?
upd:
My problem is if connection is fails by timeout - in business logic i must recreate connection with similar request. If i have got horrible connection for 1 min and after that connection will be good - server will get a lot of (about 3 times retry count) photos. But first 2 connections is canceled. –
At the moment i make "dirty hack" like "if it's photo uploading request" - do not retry recreate connection.
I do a ton of network stuff and don't recall a scenario where everything was successfully received but the iOS app timed out. I'm trying to grok the scenario you describe, where you're seeing this happen a lot and I'm not seeing how that would happen. We might need to see some of your code.
Regardless, when you cancel a NSURLConnection, it not only stops the delegate methods from being called, but it stops the upload, too. I just did a test:
I attempting to upload a 20mb file (non-chunked request);
At the 1mb mark (as identified by didSendBodyData), I canceled the connection (by calling [connection cancel]);
I immediately stopped receiving any delegate messages at that point;
Looking at Charles, I'm only seeing 1.3mb of data in the hex log of the request. When I look at the "Network" tab of the Mac OS "Activity Monitor" and looking at by "Sent Bytes", it's at 2.1mb uploaded.
So canceling a connection will stop further data from being sent. Perhaps if there is some transmission in progress that still gets out (that's the asynchronous world we live it), but the it's not true to conclude that canceled connections will routinely send their full HTTP request. There must be something about the nature of the timeout that is unique to your environment.
In terms of your immediate problem, I might suggest that when uploading a file that the iOS app assign some unique identifier to the upload so that the server code can immediately recognize duplicate requests and handle them appropriately. But the question is why you are seeing so many time-outs and notably ones where the request appears to be successfully received in toto, but the response is not. That's very curious.
You cannot forcefully abort an ongoing connection.
In case if connection is not yet started cancel and unscheduleFromRunLoop for the NSURLConnection will work.
Try with following step
[myConnection cancel];
myConnection = nil;
Might be helpful in your case and If this step is not working then also try with myConnection.delegate = nil;