nextcloud / nextcloud/ios

Auto-upload discovery bails out on not-yet-known reachability leading to deferred uploads

Open
#4,308 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Swift
Stars
2.5k
Forks
1k
Avg merge
2d 18h
Merged PRs (30d)
13

Description

How to use GitHub
  • Please use the πŸ‘ reaction to show that you are affected by the same issue.
  • Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
  • Subscribe to receive notifications on status change and new comments.

Bug report

Steps to reproduce
  1. activate auto upload
  2. use phone, take pictures
  3. use Nextcloud app with functional location updates (#4305)
Expected behaviour

Pictures get uploaded as soon as possible

Actual behaviour

Some pictures don't get uploaded until later

Logs

See below

Reasoning or why should it be changed/implemented?

Get a smooth experience with Nextcloud App. Note: during my many experiments, I found evidence that background tasks run budget is quite small : 2-30 s, with a median closer to 2 than to 30, which on an iPhone12 mini accounts for about 10 photos discovered at most per run. Also, tasks get fired at most every 8 minutes (sometimes hours). Altogether this requires user to foreground app often just to make sure pictures are uploaded at regular intervals, which IMHO is not the intention.

Environment data

Code references are against upstream master at
204f9e23b1 (2026-08-23). Log lines come from a build with diagnostic logging added locally
(getCameraRollAssets: counts, Location update delivered, Re-arming location monitoring at launch,
Auto upload skipped: network not reachable); upstream logs only Triggered by location change and
Auto upload found N new items. Coordinates that appeared in older logs are redacted.

Device: iPhone 12 mini, iOS 26.6.1 "Always" location authorization, auto-upload on, Wi-Fi off during the outings.

Precondition. This path is only reachable once a process relaunched by iOS for a location event
re-arms monitoring and receives the event, see (#4305). On upstream master that
never happens, so the defect below is latent there; it becomes the very next failure the moment the
location relaunch works.


Analysis

Summary

A discovery pass triggered by a significant-location-change delivery at a cold background launch returns
Auto upload found 0 new items without querying the photo library, every time, with photos waiting.
A BGAppRefreshTask pass in the same process one second later queries the library normally. The only
early return that precedes the library query is guard self.networking.isOnline, and isOnline is
false while reachability has not yet been observed β€” which is the state of a process one to three
seconds into a cold launch.


Apple's contract

The location event is delivered synchronously, inside didFinishLaunchingWithOptions β€”
startMonitoringSignificantLocationChanges():

Upon relaunch, you must still configure a location manager object and call this method to continue
receiving location events. When you restart location services, the current event is delivered to your
delegate immediately.

So the delegate β€” and whatever it kicks off β€” runs at the earliest possible moment of the process's life,
before any asynchronous observer set up earlier in the same launch method has had a chance to fire.

Queueing work while connectivity is unknown is safe for a background session β€”
URLSessionConfiguration.waitsForConnectivity:

This property is ignored by background sessions, which always wait for connectivity.

Discovery and queueing (a PhotoKit query plus Realm writes) need no network at all; the transfers they
queue go on a background URLSession, which by contract waits for connectivity on its own. Nothing in the
discovery step therefore depends on reachability being known.


Where the code departs from it

iOSClient/Networking/NCAutoUpload.swift (master, lines 20–23):

func initAutoUpload(controller: NCMainTabBarController? = nil) async -> Int {
    guard self.networking.isOnline else {
        return 0
    }

iOSClient/Networking/NCNetworking.swift (master, lines 74 and 85–87):

var networkReachability: NKTypeReachability?
…
var isOnline: Bool {
    return networkReachability == NKTypeReachability.reachableEthernetOrWiFi || networkReachability == NKTypeReachability.reachableCellular
}

networkReachability is an optional that starts nil. It is only ever assigned in
NCNetworking+NextcloudKitDelegate.swift:33, from networkReachabilityObserver(_:), which NextcloudKit
calls from Alamofire's NetworkReachabilityManager.startListening. Alamofire (5.12.0,
NetworkReachabilityManager.swift:172–218) delivers the initial status asynchronously: it hops to its
private reachabilityQueue, then to the listener's queue (.main). Until both hops have run, isOnline
is false β€” indistinguishable from "offline". (NKTypeReachability.unknown is also treated as offline by
both isOnline and isOffline.)

Ordering inside application(_:didFinishLaunchingWithOptions:) on a build where the relaunch re-arms
monitoring: NextcloudKit.shared.setup(…) (which starts the reachability listener) runs first; the
location manager is armed later in the same method and, per the contract above, delivers the current
event to didUpdateLocations immediately; that fires autoUploadBackgroundSync(), whose first act is
initAutoUpload(), whose first act is the guard above. The reachability listener's two asynchronous
hops have not happened yet.

Between the guard and the photo-library query there is exactly one other early exit β€” no account with
autoUploadStart == true β€” and it is ruled out below by a pass in the same process one second later
that queried the library.


Field evidence

The symptom β€” log-g, 2026-09-15, walk with the app force-quit beforehand

Every location-triggered pass at a cold launch reports zero without the getCameraRollAssets: line
that every real query emits; a BGTask pass in the same process a second later emits it:

15:00:51  [START] Start session …                                  ← cold launch
15:00:54  [LOCATION] Triggered by location change: <redacted>
15:00:54  [BGSYNC] Auto upload found 0 new items                   ← no getCameraRollAssets: line β€” bailed
15:00:55  [BGT] Start processing task
15:00:55  [BGT] Start refresh task
15:00:55  [BGSYNC] getCameraRollAssets: collections=1, filteredCount=0, unfilteredLibraryCount=2   ← queried fine, 1 s later
15:00:55  [BGSYNC] Auto upload found 0 new items

15:18:31  [DEBUG] App is terminating                               ← tester force-quits; 7 photos taken 15:18:41–15:30:04

15:35:11  [START] Start session …                                  ← relaunched for a location event, mid-walk
15:35:12  [LOCATION] Triggered by location change: <redacted>
15:35:12  [BGSYNC] Auto upload found 0 new items                   ← bailed; 7 photos waiting, oldest 17 min old

16:01:06  [START] Start session …                                  ← relaunched on arrival home
16:01:08  [LOCATION] Triggered by location change: <redacted>
16:01:08  [BGSYNC] Auto upload found 0 new items                   ← bailed; 7 photos waiting, oldest 43 min old

16:09:50  [BGT] Start refresh task                                 ← same process as 16:01:06 (no [START] between)
16:09:50  [BGSYNC] getCameraRollAssets: collections=1, filteredCount=7, unfilteredLibraryCount=9
16:09:51  [DEBUG] Automatic upload, new 7 assets found             ← BGTask, 9 min later, finds them
16:09:52  Uploading file 26-09-15 15-18-41 0808.heic …             ← capture times 15:18:41 … 15:30:04
…
16:22:46  [START] Start session …
16:22:47  [LOCATION] Triggered by location change: <redacted>
16:22:47  [BGSYNC] Auto upload found 0 new items                   ← bailed
16:22:47  [BGT] Start refresh task
16:22:47  [BGSYNC] getCameraRollAssets: collections=1, filteredCount=0, unfilteredLibraryCount=9   ← same second, queried fine

Four location-triggered passes at cold launch, four bail-outs, zero library queries. The 15:35 and 16:01
ones had seven photos waiting (captured 15:18:41–15:30:04, confirmed by the filenames uploaded at 16:09).
The BGTask passes at 15:00:55 and 16:22:47 β€” same process, one second later or the same second β€” ran the
query, which rules out the "no auto-upload account" exit and leaves the reachability guard as the only
path that skips the query.

Contrast log-f earlier the same day, where the location delivery arrived in a process the tester had
foregrounded (reachability long since observed):

12:57:01  [DEBUG] Application will enter in foreground
12:57:04  [LOCATION] Triggered by location change: <redacted>
12:57:04  [BGSYNC] getCameraRollAssets …                           ← queried

Same trigger, same code, different process age: the guard passes.

After treating "not yet known" as not-offline β€” log-h, 2026-09-15, walk
16:47:29  [DEBUG] App is terminating                               ← terminated by the install
17:13:54  [START] Start session …                                  ← relaunched 3 min after leaving home
17:13:54  [LOCATION] Re-arming location monitoring at launch
17:13:54  [LOCATION] Location update delivered
17:13:55  [LOCATION] Triggered by location change
17:13:56  [BGSYNC] getCameraRollAssets: authStatus=3, hasPermission=true, autoUploadSinceDate=…
17:13:56  [BGSYNC] getCameraRollAssets: collections=1, filteredCount=2, unfilteredLibraryCount=4
17:13:56  [DEBUG] Automatic upload, new 2 assets found
17:13:58  Uploading file 26-09-15 17-11-53 0815.heic with taskIdentifier 1 on session com.nextcloud.session.uploadbackground
17:14:01  Uploading file 26-09-15 17-12-16 0816.heic with taskIdentifier 2 …
17:19:47  [LOCATION] Location update delivered                     ← second delivery, same process
17:19:47  [BGSYNC] getCameraRollAssets: collections=1, filteredCount=2, unfilteredLibraryCount=6
17:19:48  Uploading file … 0817.heic … / … 0818.heic …

Same launch shape as log-g (trigger one to two seconds after [START]), now going through to the query
and dispatching within four seconds of the process starting. Every location-triggered cold-launch pass
since (log-i 20:54:33 β†’ 4 assets; log-i 08:28:11 β†’ 35 assets; log-j four passes) has queried the library.

What the guard still does when the device is genuinely offline β€” log-i

The replaced guard skips only on a known .notReachable, and logs it. Both occurrences coincide with
NSURLErrorDomain -1009 on independent requests in the same second, i.e. real loss of connectivity, not
a not-yet-known state:

06:38:39  Network response result: failure(… Code=-1009 "La connexion Internet semble interrompue." …)
06:38:39  [BGSYNC] Auto upload skipped: network not reachable
06:38:44  Network response result: success(170 bytes)             ← status.php, 5 s later: radio came up
08:40:37  [LOCATION] Location update delivered
08:40:38  [BGSYNC] Auto upload skipped: network not reachable
08:40:38  Network response result: failure(… Code=-1009 …)          ← Γ—3, PROPFINDs for queued uploads

In both cases the pass skipped discovery and nothing re-ran it when connectivity returned five seconds
(06:38) or three minutes (08:40 β†’ 08:43 foreground) later. Given the waitsForConnectivity contract
above, discovery had no reason to be gated on reachability in the first place; that observation is
recorded here without prescribing a change.


Test scripts (as written by the tester)

log-g (test-g.md)

- 15:18 : force kill the backgrounded app, take a few pictures, take wifi off, lock screen
- 15:22 start walking -- approx 700m away in straight line and back, but I walked over 1,5km total
- 16:01-16:03 : elevator, home
- 16:04 : charge. At this stage there is no visible (complete) upload on the server
- 16:10-16:11 : pictures uploaded
- 16:30 : foreground and extract log

log-h (test-h.md)

- 3G, 4G, but mostly good 5G reception available throughout the test except briefly after start and arrival
- took 11 photos throughout the test : 17:11 (815), 17:12, 17:13 17:17 17:19 17:19, 17:20 17:22 17:24 17:28 17:34 (825)
- start moving 17:11
- arrival 800m away straight line 17:24
- turn back, arrival back home 17:34
- set to charge at 17:35-36
- at 17:37, notice nextcloud "activity" page says 3 photos were uploaded at 17:35:56 : 815, 816, 817
- 17:50: foreground app, wait for it to settle, extract log-h.txt at 17:51

Appendix β€” every location-triggered pass at a cold launch, before and after

"Cold" = Triggered by location change within 3 s of a [START] Start session line with no foreground
in between. "Queried" = a getCameraRollAssets: line follows the trigger.

Log Trigger Seconds after [START] Queried Result
log-g 15:00:54 3 no 0 (bailed)
log-g 15:35:12 1 no 0 (bailed), 7 photos waiting
log-g 16:01:08 2 no 0 (bailed), 7 photos waiting
log-g 16:22:47 1 no 0 (bailed)
log-h 17:13:55 1 yes 2 found, 2 dispatched
log-i 20:54:34 1 yes 4 found, 4 dispatched

(log-i 08:28:11 and log-j's four triggers were deliveries into already-running processes and are not
cold-launch cases; all of them queried.)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up β€” it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in iOSClient/Networking/NCAutoUpload.swift at initAutoUpload(), then read NCNetworking.swift and NCNetworking+NextcloudKitDelegate.swift to understand how reachability is initialized. Reproduce a cold location-triggered launch with pending photos and compare it with a later BGTask pass. Done means discovery is not skipped while reachability is unknown, while genuinely offline behavior remains covered.

Written by the indexing model from the issue text.

Assessment

Tech stack
ios, swift
Domain
mobile
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.