NWConnection: how to recover data connection after RF cellular data connection loss

iOS Development environment

Xcode 16.4, macOS 15.6.1 (24G90) Run-time configuration: iOS 17.2+

Short Description

After having successfully established an NWConnection (either as UDP or TCP), and subsequently receiving the error code:

UDP Connection failed: 57 The operation couldn't be completed. (Network.NWError error 57 - Socket is not connected), available Interfaces: [enO]

via NWConnection.stateUpdateHandler = { (newState) in ... } while newState == .failed

the data connection does not restart by itself once cellular (RF) telephony coverage is established again.

Detailed Description

Context: my app has a continuous cellular data connection while in use. Either a UDP or a TCP connection is established depending on the user settings.

The setup data connection works fine until the data connection gets disconnected by loss of connection to a available cellular phone base station. This disconnection simply occurs in very poor UMTS or GSM cellular phone coverage. This is totally normal behavior in bad reception areas like in mountains with signal loss.

STEPS TO REPRODUCE

Pre-condition

App is running with active data connection.

Action

iPhone does loss the cellular data connection previously setup. Typically reported as network error code 57.

Observed

The programmed connection.stateUpdateHandler() is called in network connection state '.failed' (OK). The self-programmed data re-connection includes:

  • a call to self.connection.cancel()
  • a call to self.setupUDPConnection() or self.setupConnection() depending on the user settings to re-establish an operative data connection.

However, the iPhone's UMTS/GSM network data (re-)connection state is not properly identified/notified via NWConnection API. There's no further network state notification by means of NWConnection even though the iPhone has recovered a cellular data network.

Expected

The iPhone or any other means automatically reconnects the interrupted data connection on its own. The connection.stateUpdateHandler() is called at time of the device's networking data connection (RF) recovering, subsequently to a connection state failed with error code 57, as the RF module is continuously (independently from the app) for available telephony networks.

QUESTION

How to systematically/properly detect a cellular phone data network reconnection readiness in order to causally reinitialize the NWConnection data connection available used in app.

Relevant code extract

Setup UDP connection (or similarly setup a TCP connection)

    func setupUDPConnection() {
        let udp = NWProtocolUDP.Options.init()
        udp.preferNoChecksum = false
        let params = NWParameters.init(dtls: nil, udp: udp)
        params.serviceClass = .responsiveData       // service type for medium-delay tolerant, elastic and inelastic flow, bursty, and long-lived connections
        connection = NWConnection(host: NWEndpoint.Host.name(AppConstant.Web.urlWebSafeSky, nil), port: NWEndpoint.Port(rawValue: AppConstant.Web.urlWebSafeSkyPort)!, using: params)

        connection.stateUpdateHandler = { (newState) in
            
            switch (newState) {
            case .ready:
                //print("UDP Socket State: Ready")
                self.receiveUDPConnection(). // data reception works fine until network loss
                break
            case .setup:
                //print("UDP Socket State: Setup")
                break
            case .cancelled:
                //print("UDP Socket State: Cancelled")
                break
            case .preparing:
                //print("UDP Socket State: Preparing")
                break
            case .waiting(let error):
                Logger.logMessage(message: "UDP Connection waiting: "+error.errorCode.description+" \(error.localizedDescription), available Interfaces: \(self.connection.currentPath!.availableInterfaces.description)", LoggerLevels.Error)
                break
            case .failed(let error):
                Logger.logMessage(message: "UDP Connection failed: "+error.errorCode.description+" \(error.localizedDescription), available Interfaces: \(self.connection.currentPath!.availableInterfaces.description)", LoggerLevels.Error)

                // data connection retry (expecting network transport layer to be available)
                self.reConnectionServer()
                break
            default:
                //print("UDP Socket State: Waiting or Failed")
                break
            }
            
            self.handleStateChange()
        }
        connection.start(queue: queue)
    }

Handling of network data connection loss

    private func reConnectionServer() {
        self.connection.cancel()
        // Re Init Connection - Give a little time to network recovery
        let delayInSec = 30.0. // expecting actually a notification for network data connection availability, instead of a time-triggered retry
        self.queue.asyncAfter(deadline: .now() + delayInSec) {
            switch NetworkConnectionType {
            case 1:
                self.setupUDPConnection() // UDP
                break
            case 2:
                self.setupConnection() // TCP
                break
            default:
                break
            }
        }
    }

Does it necessarily require the use of CoreTelephony class CTTelephonyNetworkInfo or class CTCellularData to get notifications of changes to the user’s cellular service provider?

So, what I’d expect to see in your situation is:

  1. Your first connection enters the .failed(…) state with some error.
  2. You start a new connection.
  3. It enters the .waiting(…) state, because no network is available.
  4. When the network becomes available, it connects and enters the .ready state.

Note Step 1 isn’t guaranteed. When trying to maintain a long-term connection it’s a good idea to viability handler (viabilityUpdateHandler) and a better path handler (betterPathUpdateHandler) to catch path changes, and to maintain a continuous receive request to catch the TCP stream ending. However, I’m not digging into those here because you post makes it clear that you are receiving the .failed(…) state.

In the past I’ve tested this sort of scenario using Airplane Mode and it worked as expected. You’re testing with bad reception, which I would expect to behave the same. So, as a first step, I’d like you to try this with Airplane Mode. If you can reproduce your problem using that, that’ll make it a lot easier to investigate.

Oh, and one more thing. You wrote:

my app has a continuous cellular data connection while in use.

Does this connection have to go over WWAN? Because the snippets you posted don’t enforce that — that is, you don’t set the requiredInterfaceType property of params to .cellular [edit: replaced placeholders with real values] — and so your connection can use any available interface. That complicates things.

Share and Enjoy

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

Hey Quinn, Thanks for your feedback.

Yes, the connection has to go via cellular network as the users are on the road.

Refering to your mentioned steps:

  1. first connection enters the .failed(…) state once in poor reception area. This works perfect.

No further states are reported any more for the same connection. Here's a trace of the states and error codes from 1st connection onwards:

... celluar network available ...

[20:49:47.GMT+2] [NetworkManager:setupUDPConnection(lat:lon:):43 5] UDP in .preparing, available Interfaces: [pdp_ip0] --> OK

[20:49:54.GMT+2] [NetworkManager:setupUDPConnection(lat:lon:):42 3] UDP in .ready, available Interfaces: [pdp_ip0] --> OK

... celluar network loss ...

[21:15:12.GMT+2] [NetworkManager:setupUDPConnection():44 2] UDP in .failed: 57 The operation couldn't be completed. (Network.NWError error 57 - Socket is not connected), available Interfaces: [pdp_ip0] --> OK, state .failed

[21:15:12.GMT+2] [NetworkManager:receiveUDPConnection ():912] Optional (unsatisfied (No network route)):The operation couldn't be completed. (Network.NWError error 57 - Socket is not connected) --> OK, state .failed (on downlink)

[21:15:12.GMT+2] [NetworkManager:sendADSLdata():778] unsatisfied (No network route):POST beacons The operation couldn't be completed. (Network.NWError error 57 - Socket is not connected) --> OK, state .failed (on uplink)

... celluar network available (again) ...

Even though the cellular network is available again (confirmed by load of a web site from within the app), the network state remains in .failed, and does not get updated anymore.

So, the question is: should connection.stateUpdateHandler report a state update even a connection state previously turned .failed?

You've mentioned the connection state should return to .ready under recovery of a strong network availability. This isn't the case.

Is this already an origin of the issue?

should connection.stateUpdateHandler report a state update even a connection state previously turned .failed?

No. .failed(_:) is a terminal state. Once a specific connection gets into that state, it can’t leave (except maybe to enter .cancelled).

However, my reading of your code (specifically, that setupUDPConnection() method) is that you create a second connection. Is that right?

And in that case, I’d expect that second connection to enter the .waiting(_:) state.

Share and Enjoy

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

OK, that"s what I've supposed. The state .failed is terminal.

This is why I've implemented the re-connection with setupUDPConnection().

However, I'm not quite sure from where to recover.

Here's my condensed implementation of setupUDPConnection():

        self.connection.cancel()
        let udp = NWProtocolUDP.Options.init()
        udp.preferNoChecksum = false
        let params = NWParameters.init(dtls: nil, udp: udp)
        params.serviceClass = .responsiveData
        connection = NWConnection(host: NWEndpoint.Host.name(AppConstant.Web.urlWebSafeSky, nil), port: NWEndpoint.Port(rawValue: AppConstant.Web.urlWebSafeSkyPort)!, using: params)
        connection.stateUpdateHandler = { (newState) in 
            switch (newState) {
            case .ready:
                self.receiveUDPConnection()
                break
            case .setup:
// Nop
                break
            case .cancelled:
// Nop
                break
            case .preparing:
// Nop
                break
            case .waiting(let error):
// Nop
                break
            case .failed(let error):
// Cancel present connection
//                 break
            default:
                break
            }
            
            self.handleStateChange()
        }
        connection.start(queue: queue)`

But the connection using the same NWConnection instance (self.connection) does not succeed. Possibly, because the network reception is still weak. How does one get informed about re-availability of the RF network connection in order to start the above mentioned setupUDPConnection()?

But the connection using the same NWConnection instance (self.connection) does not succeed.

I’m actually a bit confused here, because your code uses two different syntaxes to refer to the connection. With reference to your most recent snippet, line 1 is this:

self.connection.cancel()

which uses self.connection and line 6 is this:

connection = NWConnection(…)

which just uses connection. I believe that these are referencing the same thing, that is, the connection property of the enclosing class. However, it’s hard to be 100% certain without seeing more context.

Stilly, I can explain some basics…

NWConnection is a class, so self.connection is a reference to that class. So, when talking about “the same connection” you have to distinguish between an object reference and the object itself.

Any given NWConnection object has a one way state flow. It starts in the .setup state and ends in either .failed(_:) or .cancelled. Thus, once you cancel a connection object, it will never reconnect.

However, your code is working with object references. If you write code like this:

self.connection.cancel()
let c = NWConnection(…)
self.connection = c

then you’re dealing with a single object reference — the self.connection property — but two objects. The first line cancels the first object. The second line creates a new object. And the third line updates the object reference to refer to the new object, releasing the reference to the old object and retaining a long-term reference to the new one.

So, my reading of your code is that when you call setupUDPConnection(…) you are creating a new object, and that should start in the .setup start and then move from that to one of the later states when you call start(queue:). However, that analysis is based on whether connection and self.connection are the same thing.

If you want to be sure which object is which, you can create an ObjectIdentifier from your reference [1]:

let id = ObjectIdentifier(connection)

I recommend that you add that to your logging to make sure you’re working with a new connection object when you try to reconnect.

Share and Enjoy

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

[1] In the new Network framework API, we added an id property, which super useful in cases like this.

NWConnection: how to recover data connection after RF cellular data connection loss
 
 
Q