How to set a timeout with AFNetworking

69,246

Solution 1

Changing the timeout interval is almost certainly not the best solution to the problem you're describing. Instead, it seems like what you actually want is for the HTTP client to handle the network becoming unreachable, no?

AFHTTPClient already has a built-in mechanism to let you know when internet connection is lost, -setReachabilityStatusChangeBlock:.

Requests can take a long time on slow networks. It's better to trust iOS to know how to deal slow connections, and tell the difference between that and having no connection at all.


To expand on my reasoning as to why other approaches mentioned in this thread should be avoided, here are a few thoughts:

  • Requests can be cancelled before they're even started. Enqueueing a request makes no guarantees about when it actually starts.
  • Timeout intervals shouldn't cancel long-running requests—especially POST. Imagine if you were trying to download or upload a 100MB video. If the request is going along as best it can on a slow 3G network, why would you needlessly stop it if it's taking a bit longer than expected?
  • Doing performSelector:afterDelay:... can be dangerous in multi-threaded applications. This opens oneself up to obscure and difficult-to-debug race conditions.

Solution 2

I strongly recommend looking at mattt's answer above - although this answer doesn't fall foul of the problems he mentions in general, for the original posters question, checking reachability is a much better fit.

However, if you do still want to set a timeout (without all the problems inherent in performSelector:afterDelay: etc, then the pull request Lego mentions describes a way to do this as one of the comments, you just do:

NSMutableURLRequest *request = [client requestWithMethod:@"GET" path:@"/" parameters:nil];
[request setTimeoutInterval:120];

AFHTTPRequestOperation *operation = [client HTTPRequestOperationWithRequest:request success:^{...} failure:^{...}];
[client enqueueHTTPRequestOperation:operation];

but see the caveat @KCHarwood mentions that it appears Apple don't allow this to be changed for POST requests (which is fixed in iOS 6 and upwards).

As @ChrisopherPickslay points out, this isn't an overall timeout, it's a timeout between receiving (or sending data). I'm not aware of any way to sensibly do an overall timeout. The Apple documentation for setTimeoutInterval says:

The timeout interval, in seconds. If during a connection attempt the request remains idle for longer than the timeout interval, the request is considered to have timed out. The default timeout interval is 60 seconds.

Solution 3

You can set the timeout interval through requestSerializer setTimeoutInterval method.You can get the requestSerializer from an AFHTTPRequestOperationManager instance.

For example to do a post request with a timeout of 25 second :

    NSDictionary *params = @{@"par1": @"value1",
                         @"par2": @"value2"};

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager.requestSerializer setTimeoutInterval:25];  //Time out after 25 seconds

    [manager POST:@"URL" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {

    //Success call back bock
    NSLog(@"Request completed with response: %@", responseObject);


    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
     //Failure callback block. This block may be called due to time out or any other failure reason
    }];

Solution 4

I think you have to patch that in manually at the moment.

I am subclassing AFHTTPClient and changed the

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters

method by adding

[request setTimeoutInterval:10.0];

in AFHTTPClient.m line 236. Of course it would be good if that could be configured, but as far as I see that is not possible at the moment.

Solution 5

Finally found out how to do it with an asynchronous POST request:

- (void)timeout:(NSDictionary*)dict {
    NDLog(@"timeout");
    AFHTTPRequestOperation *operation = [dict objectForKey:@"operation"];
    if (operation) {
        [operation cancel];
    }
    [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
    [self perform:[[dict objectForKey:@"selector"] pointerValue] on:[dict objectForKey:@"object"] with:nil];
}

- (void)perform:(SEL)selector on:(id)target with:(id)object {
    if (target && [target respondsToSelector:selector]) {
        [target performSelector:selector withObject:object];
    }
}

- (void)doStuffAndNotifyObject:(id)object withSelector:(SEL)selector {
    // AFHTTPRequestOperation asynchronous with selector                
    NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
                            @"doStuff", @"task",
                            nil];

    AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:baseURL]];

    NSMutableURLRequest *request = [httpClient requestWithMethod:@"POST" path:requestURL parameters:params];
    [httpClient release];

    AFHTTPRequestOperation *operation = [[[AFHTTPRequestOperation alloc] initWithRequest:request] autorelease];

    NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
                          operation, @"operation", 
                          object, @"object", 
                          [NSValue valueWithPointer:selector], @"selector", 
                          nil];
    [self performSelector:@selector(timeout:) withObject:dict afterDelay:timeout];

    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {            
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeout:) object:dict];
        [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
        [self perform:selector on:object with:[operation responseString]];
    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NDLog(@"fail! \nerror: %@", [error localizedDescription]);
        [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeout:) object:dict];
        [[AFNetworkActivityIndicatorManager sharedManager] decrementActivityCount];
        [self perform:selector on:object with:nil];
    }];

    NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
    [[AFNetworkActivityIndicatorManager sharedManager] incrementActivityCount];
    [queue addOperation:operation];
}

I tested this code by letting my server sleep(aFewSeconds).

If you need to do a synchronous POST request, do NOT use [queue waitUntilAllOperationsAreFinished];. Instead use the same approach as for the asynchronous request and wait for the function to be triggered which you pass on in the selector argument.

Share:
69,246

Related videos on Youtube

jennas
Author by

jennas

I'm a Full-Stack developer with over 8 years professional experience in both front and back end application development. I have a particular interest in following good practice such as using design patterns, dependency injection, responsive design and modern Javascript techniques.

Updated on July 05, 2022

Comments

  • jennas
    jennas almost 2 years

    My project is using AFNetworking.

    https://github.com/AFNetworking/AFNetworking

    How do I dial down the timeout? Atm with no internet connection the fail block isn't triggered for what feels like about 2 mins. Waay to long....

    • mattt
      mattt about 12 years
      I strongly advise against any solution that attempts to override timeout intervals, particularly the ones using performSelector:afterDelay:... to manually cancel existing operations. Please see my answer for more details.
  • kcharwood
    kcharwood over 12 years
    Another thing to consider is that Apple overrides the timeout for a POST. Automatically something like 4 minutes I think, and you CAN'T change it.
  • borisdiakur
    borisdiakur over 12 years
    Yes, I tried it out and it does not work when doing a POST request.
  • mattt
    mattt about 12 years
    No no no no no, please do not use this in a real application. What your code actually does is cancel the request after a time interval that starts when the operation is created, not when it's started. This could cause requests to be cancelled before even started.
  • borisdiakur
    borisdiakur about 12 years
    @mattt Please provide a code sample that works. Actually what you describe is exactly what I want to happen: I want the time interval to start ticking right in the moment when I create the operation.
  • slatvick
    slatvick almost 12 years
    I see it also. Why is it so? It's not good to make user waiting a 4 minute before connection fails.
  • ADAM
    ADAM over 11 years
    To add to @KCHarwood response. As of iOS 6 apple does not override the timeout of the post. This has been fixed in iOS 6.
  • Raptor
    Raptor about 11 years
    sidenote: Apple fixed this in iOS 6
  • Corey
    Corey almost 11 years
    Thanks Lego! I'm using AFNetworking and randomly about 10% of the time my operations from AFNetworking never call the success or failure blocks [server is in US, test user is in China]. This is a big problem for me as I'm intentionally blocking parts of the UI while those requests are in progress to prevent the user from sending too many requests at once. I eventually implemented a version of based on this solution that passes a completion block as a parameter via the performselector withdelay and makes sure that block is executed if !operation.isFinished - Mattt: Thank you for AFNetworking!
  • Mihai Timar
    Mihai Timar over 10 years
    I believe it's because the question is, despite it's title, about how to fail as quickly as possible in "No internet" cases. The correct way to do that is using reachability. Using timeouts either doesn't work or has a high chance of introducing hard to notice/fix bugs.
  • Christopher Pickslay
    Christopher Pickslay over 10 years
    This doesn't do what you're suggesting it does. timeoutInterval is an idle timer, not a request timeout. So you'd have to receive no data at all for 120 seconds for the above code to time out. If data slowly trickles in, the request could continue indefinitely.
  • JosephH
    JosephH over 10 years
    It's a fair point that I'd not really said much about how it does work - I've hopefully added this info in now, thanks!
  • Stavash
    Stavash over 10 years
    @mattt although I do agree with your approach, I'm struggling with a connectivity issue where I truly need timeout functionality. The thing is - I'm interacting with a device that exposes a "WiFi hotspot". When connected to this "WiFi network", in terms of reachability I'm not connected though I am able to perform requests to the device. On the other hand, I do want to be able to determine whether the device itself is reachable. Any thoughts? Thanks
  • Stavash
    Stavash over 10 years
    @mattt I have tried creating a AFNetworkReachabilityManager for a specific address, though there seems to be a problem with the callback - although I configure a status change block, the networkReachabilityStatusBlock is nil by the time there's a callback within startMonitoring. I'll open an issue via gitHub.
  • Stavash
    Stavash over 10 years
    Opened as issue #1590
  • Max MacLeod
    Max MacLeod over 10 years
    good points but doesn't answer the question on how to set a timeout
  • LKM
    LKM almost 9 years
    At AFNetworking Reference, "Network reachability is a diagnostic tool that can be used to understand why a request might have failed. It should not be used to determine whether or not to make a request." at the section of 'setReachabilityStatusChangeBlock'. So I think 'setReachabilityStatusChangeBlock' is not the solution...
  • Tanner Semerad
    Tanner Semerad over 8 years
    I understand why we should think twice about manually setting a timeout, but this accepted answer doesn't answer the question. My app needs to know as soon as possible when internet connection is lost. Using AFNetworking, the ReachabilityManager doesn't detect the case where the device is connected to a Wifi hotspot, but the hotspot itself loses internet (it often takes minutes to detect it). So making a request to Google with a timeout of ~5 seconds seems to be my best option in that scenario. Unless there's something that I'm missing?
  • AechoLiu
    AechoLiu about 8 years