Question

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....

Was it helpful?

Solution

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.

OTHER TIPS

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.

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
    }];

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.

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.

Based on others' answers and @mattt's suggestion on related project issues, here is a drop-in quickie if you are subclassing AFHTTPClient:

@implementation SomeAPIClient // subclass of AFHTTPClient

// ...

- (NSMutableURLRequest *)requestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters {
  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  [request setTimeoutInterval:120];
  return request;
}

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method path:(NSString *)path parameters:(NSDictionary *)parameters constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block {
  NSMutableURLRequest *request = [super requestWithMethod:method path:path parameters:parameters];
  [request setTimeoutInterval:120];
  return request;
}

@end

Tested to work on iOS 6.

Can't we do this with a timer like this:

In .h file

{
NSInteger time;
AFJSONRequestOperation *operation;
}

In .m file

-(void)AFNetworkingmethod{

    time = 0;

    NSTtimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(startTimer:) userInfo:nil repeats:YES];
    [timer fire];


    operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
        [self operationDidFinishLoading:JSON];
    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
        [self operationDidFailWithError:error];
    }];
    [operation setJSONReadingOptions:NSJSONReadingMutableContainers];
    [operation start];
}

-(void)startTimer:(NSTimer *)someTimer{
    if (time == 15&&![operation isFinished]) {
        time = 0;
        [operation invalidate];
        [operation cancel];
        NSLog(@"Timeout");
        return;
    }
    ++time;
}

There are two different meanings on the "timeout" definition here.

Timeout as in timeoutInterval

You want to drop a request when it becomes idle (no more transfer) for longer than an arbitrary interval of time. Example: you set timeoutInterval to 10 seconds, you start your request at 12:00:00, it may transfer some data until 12:00:23, then connection will timeout at 12:00:33. This case is covered by almost all answers here (including JosephH, Mostafa Abdellateef, Cornelius and Gurpartap Singh).

Timeout as in timeoutDeadline

You want to drop a request when it reaches a deadline happening arbitrary later. Example: you set deadline to 10 seconds in the future, you start your request at 12:00:00, it may attempt to transfer some data until 12:00:23, but connection will timeout earlier at 12:00:10. This case is covered by borisdiakur.

I'd like to show how to implement this deadline in Swift (3 and 4) for AFNetworking 3.1.

let sessionManager = AFHTTPSessionManager(baseURL: baseURL)
let request = sessionManager.post(endPoint, parameters: parameters, progress: { ... }, success: { ... }, failure: { ... })
// timeout deadline at 10 seconds in the future
DispatchQueue.global().asyncAfter(deadline: .now() + 10.0) {
    request?.cancel()
}

And to give a testable example, this code should print "failure" instead of "success" because of the immediate timeout at 0.0 seconds in the future:

let sessionManager = AFHTTPSessionManager(baseURL: URL(string: "https://example.com"))
sessionManager.responseSerializer = AFHTTPResponseSerializer()
let request = sessionManager.get("/", parameters: nil, progress: nil, success: { _ in
    print("success")
}, failure: { _ in
    print("failure")
})
// timeout deadline at 0 seconds in the future
DispatchQueue.global().asyncAfter(deadline: .now() + 0.0) {
    request?.cancel()
}

Agree with Matt, you shouldn't try change the timeoutInterval. But you also should not rely on reachability check to decide weather you are gonna make the connection, you don't know until you try.

As stated by Apple document:

As a general rule, you should not use short timeout intervals, and instead, should provide an easy way for the user to cancel a long-running operation. For more information, read “Designing for Real-World Networks”.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top