Summary
When using HTTP/2, after any network change, such as switching WiFi to wired, or bringing a VPN up or down, the client goes to Disconnected and never recovers, even though the server is fully reachable on the new network. Restarting the client (or signing out/in) fixes it. The connectivity poller keeps running but every authenticated request times out (~20 s), so the account is stuck.
Steps to reproduce
- Connect the client to a server reached over HTTP/2 (e.g. OpenCloud/oCIS behind a normal TLS proxy) and let it reach the
Connected state.
- Change the network: toggle WiFi to wired, or connect/disconnect a VPN.
Expected: the client detects the new network and reconnects automatically within a poll cycle.
Actual: it stays Disconnected indefinitely; only a restart recovers it.
Root cause
The client talks HTTP/2 to the server ([sync.engine] Server "…" Using HTTP/2), so all requests are multiplexed onto one long-lived connection held by the account's persistent QNetworkAccessManager (OCC::AccessManager, Account::accessManager()).
When the network changes, the OS tears down the route under that socket. Qt does not detect the dead HTTP/2 connection. It keeps the connection object pooled and coalesces every new stream onto it. Each authenticated request then stalls until its job timeout and ends with NetworkError: "Operation timed out". Nothing evicts the dead connection, so it repeats forever.
The failure is masked by an asymmetry inside ConnectionValidator:
- Step 1:
status.php (CheckServerJob) runs on a *freshly created throwaway QNAM (src/libsync/networkjobs/checkserverjobfactory.cpp: "in order to receive all ssl errors we need a fresh QNam"). A fresh QNAM always opens a new connection, so status.php succeeds in ~20 ms every round making it look like the server is reachable.
- Step 2: authenticated
GET graph/v1.0/me (JsonJob) (src/gui/connectionvalidator.cpp, slotStatusFound()) runs on the stale account QNAM, reuses the dead HTTP/2 connection, hangs ~20 s, and finishes as a network error -> reportResult(Undefined) ->AccountState::Disconnected.
Log excerpt (network switch at 13:34:32; account/host elided):
13:32:52 [sync.engine] Server "4.0.7" Using HTTP/2
13:34:32 [gui.netinfo] Connection Status changed to: Reachability::Disconnected
13:34:33 [gui.netinfo] Reachability::Online captive portal status: false
13:34:33 [gui.account.state] ConnectionValidator already running, ignoring … Queue is blocked: false
13:34:53 [sync.networkjob.jsonapi] Network error: JsonJob … "graph/v1.0/me" … NetworkError: "Operation timed out"
13:34:53 [sync.connectionvalidator] reportResult: Undefined duration(20s,102ms)
13:34:53 [gui.account.state] state change: Connecting -> Disconnected
# then, every ~62 s, indefinitely:
13:35:53 [sync.checkserverjob] status.php returns: {…"productversion":"4.0.7"…} (SUCCESS, ~22 ms)
13:36:13 [sync.networkjob.jsonapi] Network error: JsonJob … "graph/v1.0/me" … "Operation timed out"
13:36:13 [sync.connectionvalidator] reportResult: Undefined duration(19s,999ms) ; -> Disconnected
Every authenticated request on the account QNAM (graph/v1.0/me and the periodic me/drives) times out; status.php on the fresh QNAM succeeds every time. That is evidence of a black-holed connection being reused.
Suggested fix
Drop the account QNAM's stale pooled connections so the next authenticated request dials a fresh one. clearConnectionCache() preserves cookies and auth data; it empties the reuse pool (Qt's QNetworkAccessCache::clear() removes every entry, including in-use ones, so the next request cannot coalesce onto the dead connection). The client already uses it for the analogous CA-certificate-change case in src/libsync/accessmanager.cpp.
Both changes are in src/gui/accountstate.cpp.
1) Reconnect immediately on a network change: in the reachabilityChanged handler, abort validator still stuck on the dead connection (so it stops holding the check slot and its callbacks are disconnected before we touch the cache), clear the cache, then re-check:
case NetworkInformation::Reachability::Unknown:
+ if (_connectionValidator) {
+ delete _connectionValidator;
+ }
+ _account->accessManager()->clearConnectionCache();
// the connection might not yet be established
QTimer::singleShot(0, this, [this] { checkConnectivity(false); });
break;
2) Self-heal cases that emit no reachability change (resume from sleep: cf. owncloud/client#4839, NAT idle timeout, server restart) in checkConnectivity(), just before _connectionValidator = new ConnectionValidator(account());:
if (blockJobs) {
_queueGuard.block();
}
+ if (!isConnected()) {
+ _account->accessManager()->clearConnectionCache();
+ }
_connectionValidator = new ConnectionValidator(account());
Gating (2) on !isConnected() means it runs only while disconnected/connecting (the QNAM is idle then, the validator slot is free per the existing guard and folder jobs are descheduled). So this change should not race and costs nothing during steady connected syncing. With (1) the client reconnects within a second or two of the network settling; (2) guarantees eventual recovery on the periodic poll for the no-signal cases.
Environment
- OpenCloud Desktop 3.0.3 (commit
72fe8f), virtual files plugin: off
- Qt 6.11.1, OpenSSL 3.5.7, Wayland, Linux x86_64;
QNetworkInformation backend: networkmanager
- Server: OpenCloud 4.0.7, HTTP/2
Summary
When using HTTP/2, after any network change, such as switching WiFi to wired, or bringing a VPN up or down, the client goes to Disconnected and never recovers, even though the server is fully reachable on the new network. Restarting the client (or signing out/in) fixes it. The connectivity poller keeps running but every authenticated request times out (~20 s), so the account is stuck.
Steps to reproduce
Connectedstate.Expected: the client detects the new network and reconnects automatically within a poll cycle.
Actual: it stays
Disconnectedindefinitely; only a restart recovers it.Root cause
The client talks HTTP/2 to the server (
[sync.engine] Server "…" Using HTTP/2), so all requests are multiplexed onto one long-lived connection held by the account's persistentQNetworkAccessManager(OCC::AccessManager,Account::accessManager()).When the network changes, the OS tears down the route under that socket. Qt does not detect the dead HTTP/2 connection. It keeps the connection object pooled and coalesces every new stream onto it. Each authenticated request then stalls until its job timeout and ends with
NetworkError: "Operation timed out". Nothing evicts the dead connection, so it repeats forever.The failure is masked by an asymmetry inside
ConnectionValidator:status.php(CheckServerJob) runs on a *freshly created throwaway QNAM (src/libsync/networkjobs/checkserverjobfactory.cpp: "in order to receive all ssl errors we need a fresh QNam"). A fresh QNAM always opens a new connection, sostatus.phpsucceeds in ~20 ms every round making it look like the server is reachable.GET graph/v1.0/me(JsonJob) (src/gui/connectionvalidator.cpp,slotStatusFound()) runs on the stale account QNAM, reuses the dead HTTP/2 connection, hangs ~20 s, and finishes as a network error ->reportResult(Undefined)->AccountState::Disconnected.Log excerpt (network switch at 13:34:32; account/host elided):
Every authenticated request on the account QNAM (
graph/v1.0/meand the periodicme/drives) times out;status.phpon the fresh QNAM succeeds every time. That is evidence of a black-holed connection being reused.Suggested fix
Drop the account QNAM's stale pooled connections so the next authenticated request dials a fresh one.
clearConnectionCache()preserves cookies and auth data; it empties the reuse pool (Qt'sQNetworkAccessCache::clear()removes every entry, including in-use ones, so the next request cannot coalesce onto the dead connection). The client already uses it for the analogous CA-certificate-change case insrc/libsync/accessmanager.cpp.Both changes are in
src/gui/accountstate.cpp.1) Reconnect immediately on a network change: in the
reachabilityChangedhandler, abort validator still stuck on the dead connection (so it stops holding the check slot and its callbacks are disconnected before we touch the cache), clear the cache, then re-check:2) Self-heal cases that emit no reachability change (resume from sleep: cf. owncloud/client#4839, NAT idle timeout, server restart) in
checkConnectivity(), just before_connectionValidator = new ConnectionValidator(account());:if (blockJobs) { _queueGuard.block(); } + if (!isConnected()) { + _account->accessManager()->clearConnectionCache(); + } _connectionValidator = new ConnectionValidator(account());Gating (2) on
!isConnected()means it runs only while disconnected/connecting (the QNAM is idle then, the validator slot is free per the existing guard and folder jobs are descheduled). So this change should not race and costs nothing during steady connected syncing. With (1) the client reconnects within a second or two of the network settling; (2) guarantees eventual recovery on the periodic poll for the no-signal cases.Environment
72fe8f), virtual files plugin: offQNetworkInformationbackend: networkmanager