Receiving "The request timed out." when using NSURLSession

Hi all,

I'm facing a very strange problem in my application. In particular, using the application when a request receives a timeout message, all the following ones receive a timeout too (see screenshot).

Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSUnderlyingError=0x28113a550 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, NSErrorFailingURLStringKey=https://MY_SERVER_URL, NSErrorFailingURLKey=https://MY_SERVER_URL, _kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102, NSLocalizedDescription=The request timed out.}

I'm not sure if this happens when the application enters the foreground after a background state since it's quite difficult to replicate.

Based on what exposed,

  • do you any clue why this can happen?
  • additionally, is there a way to replicate a similar behavior in a consistent way?

Waiting for a reply I thank you for your attention.

Thanks, Lorenzo

Hi Lorenzo,

At first glance this does look like a server issue. Could that be the case? But it could just as well be related to being in the background as well, yes.

But looking at your screenshot your statement:

when a request receives a timeout message, all the following ones receive a timeout too

does not seem to be true. There are still a bunch of URLSessionTasks that finish successfully but were started after your first request received a timeout. E.g. from your screenshot, the task with task identifier 172 started at 04:15.141.186 and it received timeout 30.79s later, so around 04:46. But task 179 starts at 05:05.274.264 and finishes successfully, even though it starts well after the first timeout. And so does task 20.

Task 179 is even on the same session as 172 and is also a GET request. Based on the duration I'm guessing that it wasn't handle by the local cache (but the instrument should give you this information as well). Are these two requests connecting to the same server as well? (your screenshot doesn't contain information about the host and path the request was sent to).

To get more information about what your app's state is (foreground or background), you can add the "Time Profiler" instrument to your document in addition to the "HTTP Traffic" instrument (use the Plus-button in the top right of the toolbar). Then make sure Instrument launches your app and you are not just attaching to the process after the fact. This way, you should get an "App Lifecycle" lane on your process track, which allows you to cross-reference which tasks were started (and finished) while the app was in the background or in the foreground.

I also recommend to take a closer look at the domain track for these tasks. The intervals drawn there should show you how long the underlying transactions for your tasks were blocked and whether they ever managed to send out the request or they timed out before even sending the request.

Hi Joachim,

I really appreciate your response. Thank you very much!

Below my replies / notes:

  • Regarding tasks 179 and 172, I can confirm they are not sent to the same server. I haven't included them for privacy reasons. But for sure I could share the full trace with you privately.
  • About requests in general I cannot find a way to know where they were made using the network or handled by the local cache. Can you point me in the right direction in order to find such an information?
  • Finally, concerning the latest sentence I do not have clear what you mean with "take a closer look at the domain track for these tasks".

Since you are confirming those issues could be due to background state, I've started to implement a way to handle this using beginBackgroundTaskWithName:expirationHandler: and endBackgroundTask:.

In particular, when I need to perform a request, I create both a new background task identifier and session data task like the following.

// tell the system the work could continue in background
__block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:taskName expirationHandler:^{
    
    [self.internalLock lock];
    for (NSURLSessionDataTask* dataTask in self.dataTasks) {
        [dataTask cancel];
    }
    
    if ([self.dataTasks count] > 0) {
        // manage failure here?
    }
    
    [self.dataTasks removeAllObjects];
    [self.internalLock unlock];
    
    cleanupBackgroundTaskIdentifier(backgroundTaskIdentifier);
}];

// create a data task to perform the request
__block NSURLSessionDataTask *task;
task = [self.urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
    // handling the result here       
    
    cleanupBackgroundTaskIdentifier(backgroundTaskIdentifier);
    sendResult(result);
    
    [self.internalLock lock];
    [self.dataTasks removeObject:task];
    [self.internalLock unlock];
}];

[self.internalLock lock];
[self.dataTasks addObject:task];
[self.internalLock unlock];

task.taskDescription = taskName;
[task resume];

where

void(^cleanupBackgroundTaskIdentifier)(UIBackgroundTaskIdentifier) = ^void(UIBackgroundTaskIdentifier backgroundTaskIdentifier) {
    [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskIdentifier];
    backgroundTaskIdentifier = UIBackgroundTaskInvalid;
};

It's a very old legacy code so I'm not sure this is the right way to handle such a scenario.

Finally, concerning the latest sentence I do not have clear what you mean with "take a closer look at the domain track for these tasks".

Take a look at the documentation for the HTTP Traffic instrument, the section "View the Track Hierarchy" has a screenshot showing the different tracks. The higher-level tracks give you summary information, the domain track gives you the most details about each task, including how many HTTP Transactions were made as part of it (e.g. whether redirects happened) and details like how long a transaction was blocked before it started sending its request.

About requests in general I cannot find a way to know where they were made using the network or handled by the local cache. Can you point me in the right direction in order to find such an information?

The easiest way to see this is to switch the Track Display style to "HTTP Transaction by Connection" in the domain track. The section "View Tasks and Transactions" in the docs describes how to do that. Once you selected that view, you should see lanes for each connection labeled "Connection 1", "Connection 2" and so on. If any transactions are handled by the cache instead of a connection, there will be a lane with "Local Cache". You can see an example in the WWDC session about the HTTP Traffic instrument here: https://developer.apple.com/videos/play/wwdc2021/10212?time=1455

Regarding the background handling in general: If you want your downloads or uploads to continue even when your app is in the background, you might want to look into [background sessions].(https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background) They don't require you to start and end a background task, but make handling the response a little more complex. Whether it's worth the tradeoff depends on your use-case.

Hi Joachim,

thank you very much for the detailed info.

I have additional questions for you:

  • timeoutInterval for NSURLRequest should be the same of timeoutIntervalForRequest. If the former has strict value than the latter, should NSURLSession honor its value?
  • what is the real meaning of timeoutInterval? Is it correct to represent it as the amount of time between packets?
  • what should be the correct way to handle tasks when the application goes in background? I would not use a background session.
  • why subsequent requests after the first failing one are failing too?

I can answer these questions:

  • timeoutInterval on NSURLRequest means the same thing as timeoutIntervalForRequest on NSURLSessionConfiguration, and the value on the request (if set) takes precedence.
  • timeoutInterval means if nothing happens (no upload activity, no download activity) in this period, the task should fail. There are specific situations where the timeout won't occur, such as waiting for connectivity, or handling an authentication challenge.
  • If a task is expected to run in the background, beginBackgroundTaskWithName on UIApplication is the correct approach.
  • If you are encountering unexpected timeouts, please file a feedback with a sysdiagnose after reproducing the issue. A task UUID (log the task using %@) would be greatly appreciated.
Receiving "The request timed out." when using NSURLSession
 
 
Q