In my app I want to make a network call only if I can access the internet.
Note: I'm connected to a WiFi spot that doesn't have Internet
I want to test if Internet is available. I tried using Reachability as described here and I also tried a simpler solution as described here
The problem is that with Reachability is that it returns that the Internet is reachable when it's not. The other solution takes too much time to give a response. I tried to set timeout intervals for the request and the session, but it's getting ignored.
How can I test for internet reachability in case I'm connected to a wifi with no Internet?
Here some code that I use:
- (void) postAsyncTaskWithUrl:(NSString*)urlString
andType:(NSString*)requestType
andToken:(NSString*)token
andPropertiesObject:(id)propObject
urlEncoded:(BOOL)isUrlEncoded
withSuccess:(nullable void(^)(id _Nullable))success
andFailure:(nullable void(^)(id _Nullable))failure
{
internetReachableFoo = [Reachability reachabilityWithHostname:#"www.google.com"];
__weak typeof(self) weakSelf = self;
// Internet is reachable
internetReachableFoo.reachableBlock = ^(Reachability*reach)
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:urlSt];
[request setTimeoutInterval:20]; //Ignored ... WHY?
NSURLSessionConfiguration *sessionConfigurations = [NSURLSessionConfiguration defaultSessionConfiguration];
[sessionConfigurations setTimeoutIntervalForRequest:20]; //Ignored ... WHY?
[sessionConfigurations setTimeoutIntervalForResource:20]; //Ignored ... WHY?
// NSURLSession *session = [NSURLSession sharedSession];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfigurations];
[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
}
}
}
Despite this being something that was historically done in the past, I don't believe that it's worth trying to check if the network is reachable, as it isn't something that's reliable or something that should be done before making a request in almost all cases now.
According to Apple's official documentation:
Always attempt to make a connection. Do not attempt to guess whether
network service is available, and do not cache that determination
Similar blog post from Jared Sinclair on this topic, using SCNetworkReachability (which is what the Reachability library uses under the hood):
SCNetworkReachability should only be used to influence what you do about a network request that has already failed, not an initial request that has yet to be attempted. Use a negative status to determine whether or not you attempt an automatic retry, or to tweak the user-facing language of an alert. Use a positive status to consider retrying an earlier failed request. Never prevent a user-initiated request from being attempted just because SCNetworkReachability thinks there’s not a reachable network
Borrowing from a recent Reddit thread:
Reachability is one or "these topics" in iOS world. What does it mean
that the Internet is reachable? Even if you check network interfaces,
you may not be able to reach apple.com or google.com. Even if you
reach Google, you may not be able to reach your on-prem server or
cloud. If you check whether you can reach your server of interest, why
not to send a request straight away?
TL;DR: You probably want an NSTimer.
In my app I want to make a network call only if I can access the internet.
This is not a meaningful statement. There is no test you can perform that reliably means you can "access the internet" (despite the existence of NSURLErrorNotConnectedToInternet, which doesn't actually mean what it says). For example, your code is trying to treat "www.google.com" as "the internet." But it is completely possible to have a network that permits packets to go to www.google.com, but nowhere else. Or to permit ICMP packets, but not HTTP packets. Or to permit HTTP packets, but not on certain ports. Such network configurations are not just possible, they're common, and in various ways they all can look like "not on the internet."
The only thing you can answer is "can I send a packet to a specific host and receive a packet in return?" It's not even possible to know whether you can just send a packet. If you don't get something in return, you don't know if your packet was delivered or not.
Reachability answers the question "if I tried to send a packet, would my local network stack even try?" It is basically the question of whether there is a first-hop available in the routing table. It doesn't mean you can actually connect; it can only tell you that it absolutely won't.
The timeouts you're using are based on idle-time on a session. But you never start the session, so it never idles. The timeout you're looking for is the TCP timeout, which I expect to be 4 minutes. NSURLSession can't configure it.
If you want a wall-clock "start-to-finish" timeout, then you need to run it yourself with a Timer.
If you try to load http://captive.apple.com it will return hardcoded string “Success” as a body if internet works or will redirect / fail. You can check for response starts to be 200 and body to be “success”. That’s what Apple is using to automatically show the credentials window when connected to airport like WiFi’s
Saying that
a) there’s no foolproof solution
b) you will be wasting user’s data (might be ok in your use case?)
Related
I have been struggling with an issue where NSURLConnection calls instantly fail. The device needs to be rebooted entirely or Flight Mode needs to be toggled on/off to resolve the problem. Restarting the app (swipe up) alone does not help.
Some facts:
-All URLs are HTTPS, TLS 1.2 compatible with Forward Secrecy. There are no issues with ATS and iOS 9. The error has been present since iOS 7 and remains on 9.2.
-No third party frameworks are used by the app. I use only native NSURLConnection calls that always work, except for when this odd situation occurs.
-No infrastructure/network issues - other devices on same networks (same WiFi for instance) work in the same app at the same time. Going to/from 3G/Wifi makes no difference.
-I always implement willCacheResponse to return nil.
-The service is hosted on AWS Elastic Beanstalk, so some suggested that it might be a DNS caching issue in case of IP address changes - this seems unlikely to me and should trigger multiple errors at once on different devices, which I have never seen.
-The method called is didFailWithError, instantaneously, as if there were no Internet connection on the device at all - all other apps work, however.
-The website that hosts the API used by the app can be browsed with no problems at all times. The website actually makes the same requests to fetch data.
The error code returned is -1003, kCFURLErrorCannotFindHost. I've been following a thread on Git dealing with the same issue to no avail. https://github.com/AFNetworking/AFNetworking/issues/967
I tried using NSURLRequestReloadIgnoringCacheData for all my requests, but that did not help.
With this information, will anyone care to venture a guess what I might be doing wrong? I added the bounty because I have no idea how to approach this problem - especially because it's so inconsistent. And it is definitely not a legitimate error (that is, that the domain could not be found), as the service is operating fine while this happens on random clients.
I create my request with a static method that looks like this. It's been stripped of some non-public info, but basically it just performs a POST request with JSON data. [Controller getSQLHost] just returns a URL - the base domain.
+(NSURLConnection*)initiatePOSTwithJSONDictionary:(NSDictionary*)dictionary toURL:(NSString*)urllocation withDelegate:delegate {
NSMutableDictionary *connectionDictionary = [[NSMutableDictionary alloc] init];
if (dictionary) {
[connectionDictionary setObject:dictionary forKey:#"input"];
}
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:connectionDictionary options:kNilOptions error:nil];
NSURL *url = [NSURL URLWithString:[[Controller getSQLHost] stringByAppendingString:urllocation]];
NSString *postLength = [NSString stringWithFormat:#"%i", (int)[jsonData length]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30.0];
[request setHTTPMethod:#"POST"];
[request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[request setHTTPBody:jsonData];
return [[NSURLConnection alloc] initWithRequest:request delegate:delegate];
}
Does you delegate implement connectionShouldUseCredentialStorage ? (or respond with YES)
I think the device's keychain is used when this method returns yes which may explain the persisting failure beyond the life time of the running application and why rebooting or otherwise resetting network connectivity "fixes" it. If an authentication failure has been recognized once, it may linger in the key chain for a little while which would then respond immediately without actually going to the server.
What would cause the authentication to register as a failure in the keychain in the first place may depend on a variety of factors. It could be as simple as a typo in the saved password or more convoluted such as some certificate expiration preventing the SSL layer from establishing a secure link.
You're creating NSURLConnections on the current runloop. How many are active at any one time? Have you considered using an NSOperationQueue so that you don't get bitten by load bugs?
Is your delegate thread-safe? If not, that could explain the sporadic-ness of the bug.
You say you don't see the problem often, but others do. Can you borrow their devices and maybe even them and their usage patterns and thus get to see the problem more often?
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.
Although my app is usable without any internet connection, it may exchange data with a web server (in order to show some user statistics). So I advertise the app as "needs no internet connection". Some users subsequently have turned off cellular data for my app, which should be completely fine. But when my app tries to exchange data, these users are bugged with the "Cellular data is turned off for [App Name]." dialog.
This is an annoyance to them and I want to prevent these dialogs and simply skip the whole data exchange thing.
There is Apple's Reachability Sample Code.
But although I turned off WiFi for the whole device and cellular data for the app, Reachability confirms a positive internet connection. To be more specific, it reports
Reachability Flag Status: WR t------ networkStatusForFlags
no matter whether I activated cellular data or not. Of course, when cellular data is turned off, no internet connection is actually available, so the data exchange fails. But the user is presented with the cellular data dialog anyway.
Is there any way to detect whether a internet connection is available on iOS 7 and iOS 8, taking into account the cellular data setting for a specific app – all without bugging the user every time again with the cellular data dialog?
My app currently comes without any settings panel, so I want to avoid setting up a (second, in-app) switch "don't use cellular data". Also, I don't want to restrict data exchange to a WiFi connection since it's just a 2 KB of data per session which isn't a big thing for most users.
I think the only supported way in iOS8 is to send a Ping to a known server and bug the user with the alert panel a few times. On iOS8, Apple displays the panel only twice, then skips it even if the app is restarted, maybe it will show up a day later again. (This is really bad news for ad-supported apps.)
Apple says (https://devforums.apple.com/message/1059332#1059332):
Another developer wrote in to DTS and thus I had a chance to
investigate this in depth. Alas, the news is much as I expected:
there is no supported way to detect that your app is in this state.
Nor is there a way to make a "no user interaction" network connection,
that is, request that the connection fail rather than present UI like
this.
The following articles suggest ways to use ping:
http://www.splinter.com.au/how-to-ping-a-server-in-objective-c-iphone/
http://elbsolutions.com/projects/reachability-with-simpleping-wrapper/
Try using this git project.
How to install you can see inside the Readme on git.
I also used dispatch_once to be sure the Reachability will only be initialized once. This dispatch type is sometimes very useful!
Define variable in class
BOOL _online = NO;
Initialize the variable
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
Reachability *reach =
[Reachability reachabilityWithHostname:gameApiHost];
reach.reachableBlock = ^(Reachability*reach) {
NSLog(#"REACHABLE!"); _online = YES;
};
reach.unreachableBlock = ^(Reachability*reach) {
NSLog(#"UNREACHABLE!"); _online = NO;
};
[reach startNotifier];
});
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;