iOS/tvOS seem to reuse closed sockets

Our iOS and AppleTV apps regularly send requests to various endpoints during their lifetime. Requests are sent using URLSession dataTask.

Problem: On AppleTV, when the app enters foreground again, it tries to send a request to one of these servers but the request often immediately fails with the following error:

Task <1F891A2A-727D-4AE5-A7D1-85099FFBCE5B>.<297> finished with error [-1005] Error Domain=NSURLErrorDomain Code=-1005 "La connexion réseau a été perdue." UserInfo={_kCFStreamErrorCodeKey=53, NSUnderlyingError=0x282a262b0 {Error Domain=kCFErrorDomainCFNetwork Code=-1005 "(null)" UserInfo={_kCFStreamErrorCodeKey=53, _kCFStreamErrorDomainKey=1}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <1F891A2A-727D-4AE5-A7D1-85099FFBCE5B>.<297>, _NSURLErrorRelatedURLSessionTaskErrorKey=( "LocalDataTask <1F891A2A-727D-4AE5-A7D1-85099FFBCE5B>.<297>" ), NSLocalizedDescription=La connexion réseau a été perdue., NSErrorFailingURLStringKey=https://entitlementvitis.gemtv.cloud/api/entitlement/get, NSErrorFailingURLKey=https://entitlementvitis.gemtv.cloud/api/entitlement/get, _kCFStreamErrorDomainKey=1} 

The error occurs even if the app only remains several seconds in background. It looks like the system closes the socket when the app enters background and tries to reuse one when it enters foreground.

The HTTP protocol can either be HTTP/1.1 or HTTP2 (depending on the endpoint the app sends the request to). But for endpoints using HTTP/1.1, adding the header "Connection: close" to the request sent before entering background seems to prevent the issue from occurring. However this is not a fix, especially it does not solve the problem for HTTP2.

Finally I've not been able to reproduce the issue while profiling the app with Network Instrument.

I've filed a feedback regarding this issue: https://feedbackassistant.apple.com/feedback/11990344

Related issue: This issue seems similar to another one we've always experienced: during the app lifetime (again iOS and tvOS), some requests reach a seemingly random timeout, sometime far less than the URLRequest timeoutInterval we usually set at 30s. It looks like the system sends a request through a socket it thinks is still open, but finds out later on that the socket was actually closed on the other side. When these error occurs, the system may log the following:

  • [connection] nw_read_request_report [C1] Receive failed with error "Socket is not connected"
  • [quic] quic_conn_send_frames_for_key_state_block_invoke [C1.1.1.1:2] [-056706ee9dedb1d86333a78c850bed80e55d70ee] unable to request outbound data
  • [] nw_endpoint_flow_fillout_data_transfer_snapshot copy_info() returned NULL
  • nw_connection_copy_connected_local_endpoint_block_invoke [C13] Connection has no connected path

Your help would be much appreciated (Eskimo)

It looks like the system closes the socket when the app enters background and tries to reuse one when it enters foreground.

It’s not closing the socket per se — indeed, this is all Network framework and thus there are no sockets in play — but it’s a process known as socket resource reclaim [1]. See Technote 2277 Networking and Multitasking.

Finally I've not been able to reproduce the issue while profiling the app with Network Instrument.

Right. While I haven’t look at Instruments specifically, you can’t, in general, test background execution code under the debugger because the debugger prevents the process from being suspended.

There’s two standard ways of dealing with this issue:

  • Prevent your app from suspending with outstanding network requests.

  • Deal with such errors by retrying.

I lean towards the second option. The first option made more sense with HTTP/1.1, where connections were relatively short lived, but that’s no longer the world we live in.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Which is now poorly named. In my defence, when I came up with the name there was only sockets.

Many thanks for your reply.

I indeed ended up with the two changes:

  1. For HTTP 1.1 requests, I added the "Connection: close" request header
  2. For HTTP 2 requests, I implemented the (empirical) detection of such errors and a one-time retry

Both changes allow the app to either avoid the issue (1) or seamlessly handle it (2).

Unfortunately, it does not help with the related issue (which is fortunately far less systematic) as the requests fail several (tens of) seconds after the dataTask is sent, time during which the user waits for his action to be performed or information to be displayed. But maybe this related issue is not as related as I think...

iOS/tvOS seem to reuse closed sockets
 
 
Q